From 0d6fe295a1f685e0f8b7b84d617935a2992ec549 Mon Sep 17 00:00:00 2001 From: oneRain Date: Fri, 19 Jul 2019 15:01:34 +0800 Subject: [PATCH] =?UTF-8?q?init:=20=E5=90=88=E5=B9=B6=E5=B7=A5=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Libs/UnityEngine.dll | Bin 0 -> 1345536 bytes Libs/websocket-sharp.dll | Bin 0 -> 253440 bytes LiveQuery/LiveQuery.PCL/LiveQuery.PCL.csproj | 53 + .../LiveQuery.PCL/Properties/AssemblyInfo.cs | 26 + .../LiveQuery.Test/LiveQuery.Test.csproj | 41 + LiveQuery/LiveQuery.Test/Test.cs | 10 + LiveQuery/LiveQuery.Test/packages.config | 4 + .../LiveQuery.Unity/LiveQuery.Unity.csproj | 56 + .../Properties/AssemblyInfo.cs | 26 + LiveQuery/Source/AVLiveQuery.cs | 225 ++ LiveQuery/Source/AVLiveQueryEventArgs.cs | 34 + LiveQuery/Source/AVLiveQueryExtensions.cs | 25 + RTM/RTM.PCL/Properties/AssemblyInfo.cs | 26 + RTM/RTM.PCL/RTM.PCL.csproj | 216 ++ RTM/RTM.PCL/packages.config | 4 + RTM/RTM.Test/RTM.Test.csproj | 41 + RTM/RTM.Test/Test.cs | 10 + RTM/RTM.Test/packages.config | 4 + RTM/RTM.Unity/Properties/AssemblyInfo.cs | 26 + RTM/RTM.Unity/RTM.Unity.csproj | 225 ++ RTM/Source/Internal/AVIMCorePlugins.cs | 56 + RTM/Source/Internal/Command/AVIMCommand.cs | 176 ++ .../Internal/Command/AVIMCommandRunner.cs | 92 + RTM/Source/Internal/Command/AckCommand.cs | 63 + .../Internal/Command/ConversationCommand.cs | 129 + .../Internal/Command/IAVIMCommandRunner.cs | 17 + RTM/Source/Internal/Command/MessageCommand.cs | 83 + RTM/Source/Internal/Command/PatchCommand.cs | 98 + RTM/Source/Internal/Command/ReadCommand.cs | 91 + RTM/Source/Internal/Command/SessionCommand.cs | 57 + .../DataEngine/Controller/DateTimeEngine.cs | 30 + .../DataEngine/Controller/DictionaryEngine.cs | 47 + .../DataEngine/Controller/StringEngine.cs | 36 + RTM/Source/Internal/IAVIMPlatformHooks.cs | 15 + .../Subclassing/FreeStyleMessageClassInfo.cs | 75 + .../FreeStyleMessageClassingController.cs | 210 ++ .../IFreeStyleMessageClassingController.cs | 18 + RTM/Source/Internal/Protocol/AVIMProtocol.cs | 19 + .../Internal/Router/AVRouterController.cs | 173 ++ .../Internal/Router/IAVRouterController.cs | 13 + .../Internal/Router/State/RouterState.cs | 20 + RTM/Source/Internal/Timer/IAVTimer.cs | 49 + .../Timer/Portable/AVTimer.Portable.cs | 107 + .../Internal/Timer/Unity/AVTimer.Unity.cs | 82 + .../Internal/WebSocket/IWebSocketClient.cs | 56 + .../NetCore/DefaultWebSocketClient.NetCore.cs | 174 ++ .../NetFx45/WebSocketClient.NetFx45.cs | 80 + .../DefaultWebSocketClient.Portable.cs | 164 ++ .../Unity/DefaultWebSocketClient.Unity.cs | 149 ++ .../WebSocket/Unity/websocket-sharp.dll | Bin 0 -> 254464 bytes RTM/Source/Public/AVIMAudioMessage.cs | 28 + RTM/Source/Public/AVIMBinaryMessage.cs | 42 + RTM/Source/Public/AVIMClient.cs | 1195 ++++++++++ RTM/Source/Public/AVIMConversation.cs | 1545 ++++++++++++ RTM/Source/Public/AVIMConversationQuery.cs | 181 ++ RTM/Source/Public/AVIMEnumerator.cs | 248 ++ RTM/Source/Public/AVIMEventArgs.cs | 251 ++ RTM/Source/Public/AVIMException.cs | 235 ++ RTM/Source/Public/AVIMImageMessage.cs | 246 ++ RTM/Source/Public/AVIMMessage.cs | 162 ++ .../Public/AVIMMessageClassNameAttribute.cs | 19 + .../Public/AVIMMessageFieldNameAttribute.cs | 18 + RTM/Source/Public/AVIMMessageListener.cs | 143 ++ RTM/Source/Public/AVIMNotice.cs | 57 + RTM/Source/Public/AVIMRecalledMessage.cs | 12 + RTM/Source/Public/AVIMSignature.cs | 42 + .../Public/AVIMTemporaryConversation.cs | 37 + RTM/Source/Public/AVIMTextMessage.cs | 47 + RTM/Source/Public/AVIMTypedMessage.cs | 205 ++ .../AVIMTypedMessageTypeIntAttribute.cs | 14 + RTM/Source/Public/AVRealtime.cs | 1294 ++++++++++ RTM/Source/Public/IAVIMListener.cs | 33 + RTM/Source/Public/IAVIMMessage.cs | 83 + RTM/Source/Public/ICacheEngine.cs | 13 + RTM/Source/Public/ISignatureFactory.cs | 131 + .../Listener/AVIMConversationListener.cs | 256 ++ .../Listener/ConversationUnreadListener.cs | 145 ++ RTM/Source/Public/Listener/GoAwayListener.cs | 28 + .../Public/Listener/MessagePatchListener.cs | 48 + .../Public/Listener/OfflineMessageListener.cs | 42 + RTM/Source/Public/Listener/SessionListener.cs | 58 + RTM/Source/Public/Unity/AVRealtimeBehavior.cs | 101 + Storage/Source/Internal/AVCorePlugins.cs | 384 +++ .../Internal/AppRouter/AppRouterController.cs | 103 + .../Internal/AppRouter/AppRouterState.cs | 85 + .../AppRouter/IAppRouterController.cs | 22 + .../IAVAuthenticationProvider.cs | 36 + .../Cloud/Controller/AVCloudCodeController.cs | 58 + .../Controller/IAVCloudCodeController.cs | 19 + Storage/Source/Internal/Command/AVCommand.cs | 118 + .../Internal/Command/AVCommandRunner.cs | 167 ++ .../Internal/Command/IAVCommandRunner.cs | 24 + .../Config/Controller/AVConfigController.cs | 40 + .../Controller/AVCurrentConfigController.cs | 76 + .../Config/Controller/IAVConfigController.cs | 21 + .../Controller/IAVCurrentConfigController.cs | 31 + .../Dispatcher/Unity/UnityDispatcher.cs | 102 + Storage/Source/Internal/Encoding/AVDecoder.cs | 164 ++ Storage/Source/Internal/Encoding/AVEncoder.cs | 138 ++ .../Source/Internal/Encoding/AVObjectCoder.cs | 105 + .../Internal/Encoding/NoObjectsEncoder.cs | 23 + .../Encoding/PointerOrLocalIdEncoder.cs | 72 + .../File/Controller/AVFileController.cs | 151 ++ .../File/Controller/AWSS3FileController.cs | 54 + .../File/Controller/IAVFileController.cs | 24 + .../Controller/QCloudCosFileController.cs | 250 ++ .../File/Controller/QiniuFileController.cs | 332 +++ .../Internal/File/Cryptography/MD5/MD5.cs | 566 +++++ .../SHA1/SHA1CryptoServiceProvider.cs | 495 ++++ .../Source/Internal/File/State/FileState.cs | 27 + .../Source/Internal/HttpClient/HttpRequest.cs | 25 + .../Source/Internal/HttpClient/IHttpClient.cs | 24 + .../Portable/HttpClient.Portable.cs | 163 ++ .../HttpClient/Unity/HttpClient.Unity.cs | 168 ++ Storage/Source/Internal/IAVCorePlugins.cs | 26 + .../Controller/IInstallationIdController.cs | 24 + .../Controller/InstallationIdController.cs | 66 + .../Object/Controller/AVObjectController.cs | 248 ++ .../Object/Controller/IAVObjectController.cs | 37 + .../Controller/IAVObjectCurrentController.cs | 51 + .../Internal/Object/State/IObjectState.cs | 19 + .../Object/State/MutableObjectState.cs | 114 + .../IObjectSubclassingController.cs | 24 + .../Object/Subclassing/ObjectSubclassInfo.cs | 42 + .../ObjectSubclassingController.cs | 171 ++ .../Internal/Operation/AVAddOperation.cs | 53 + .../Operation/AVAddUniqueOperation.cs | 69 + .../Internal/Operation/AVDeleteOperation.cs | 38 + .../Internal/Operation/AVFieldOperations.cs | 40 + .../Operation/AVIncrementOperation.cs | 166 ++ .../Internal/Operation/AVRelationOperation.cs | 118 + .../Internal/Operation/AVRemoveOperation.cs | 68 + .../Internal/Operation/AVSetOperation.cs | 21 + .../Internal/Operation/IAVFieldOperation.cs | 42 + .../Query/Controller/AVQueryController.cs | 110 + .../Query/Controller/IAVQueryController.cs | 21 + .../Session/Controller/AVSessionController.cs | 50 + .../Controller/IAVSessionController.cs | 15 + .../Internal/Storage/IStorageController.cs | 54 + .../Storage/NetCore/StorageController.cs | 184 ++ .../Storage/Portable/StorageController.cs | 192 ++ .../Storage/Unity/StorageController.cs | 250 ++ .../Controller/AVCurrentUserController.cs | 159 ++ .../User/Controller/AVUserController.cs | 161 ++ .../Controller/IAVCurrentUserController.cs | 11 + .../User/Controller/IAVUserController.cs | 42 + .../Internal/Utilities/AVConfigExtensions.cs | 20 + .../Internal/Utilities/AVFileExtensions.cs | 20 + .../Internal/Utilities/AVObjectExtensions.cs | 225 ++ .../Internal/Utilities/AVQueryExtensions.cs | 28 + .../Utilities/AVRelationExtensions.cs | 28 + .../Internal/Utilities/AVSessionExtensions.cs | 26 + .../Internal/Utilities/AVUserExtensions.cs | 22 + .../Utilities/FlexibleDictionaryWrapper.cs | 104 + .../Internal/Utilities/FlexibleListWrapper.cs | 81 + .../Internal/Utilities/IJsonConvertible.cs | 15 + .../Utilities/IdentityEqualityComparer.cs | 23 + .../Internal/Utilities/InternalExtensions.cs | 105 + Storage/Source/Internal/Utilities/Json.cs | 554 +++++ Storage/Source/Internal/Utilities/LockSet.cs | 41 + .../Internal/Utilities/ReflectionHelpers.cs | 123 + .../Utilities/SynchronizedEventHandler.cs | 66 + .../Source/Internal/Utilities/TaskQueue.cs | 68 + .../Internal/Utilities/XamarinAttributes.cs | 426 ++++ Storage/Source/Public/AVACL.cs | 284 +++ Storage/Source/Public/AVClassNameAttribute.cs | 29 + Storage/Source/Public/AVClient.cs | 503 ++++ Storage/Source/Public/AVCloud.cs | 599 +++++ Storage/Source/Public/AVConfig.cs | 143 ++ .../Public/AVDownloadProgressEventArgs.cs | 15 + Storage/Source/Public/AVException.cs | 275 +++ Storage/Source/Public/AVExtensions.cs | 160 ++ Storage/Source/Public/AVFieldNameAttribute.cs | 30 + Storage/Source/Public/AVFile.cs | 728 ++++++ Storage/Source/Public/AVGeoDistance.cs | 78 + Storage/Source/Public/AVGeoPoint.cs | 107 + Storage/Source/Public/AVObject.cs | 2100 +++++++++++++++++ Storage/Source/Public/AVQuery.cs | 360 +++ Storage/Source/Public/AVQueryExtensions.cs | 818 +++++++ Storage/Source/Public/AVRelation.cs | 175 ++ Storage/Source/Public/AVRole.cs | 111 + Storage/Source/Public/AVSession.cs | 111 + Storage/Source/Public/AVStatus.cs | 24 + .../Public/AVUploadProgressEventArgs.cs | 15 + Storage/Source/Public/AVUser.cs | 1544 ++++++++++++ .../Public/AVUserAuthDataLogInOption.cs | 33 + Storage/Source/Public/IAVQuery.cs | 2068 ++++++++++++++++ .../Public/LeaderBoard/AVLeaderboard.cs | 479 ++++ .../LeaderBoard/AVLeaderboardArchive.cs | 76 + .../Source/Public/LeaderBoard/AVRanking.cs | 72 + .../Source/Public/LeaderBoard/AVStatistic.cs | 52 + .../Public/Unity/AVInitializeBehaviour.cs | 75 + Storage/Source/Public/Utilities/Conversion.cs | 138 ++ .../Storage.PCL/Properties/AssemblyInfo.cs | 26 + Storage/Storage.PCL/Storage.PCL.csproj | 363 +++ Storage/Storage.PCL/packages.config | 4 + Storage/Storage.Test/Storage.Test.csproj | 48 + Storage/Storage.Test/Test.cs | 21 + Storage/Storage.Test/packages.config | 4 + .../Storage.Unity/Properties/AssemblyInfo.cs | 26 + Storage/Storage.Unity/Storage.Unity.csproj | 368 +++ csharp-sdk.sln | 87 + 202 files changed, 31267 insertions(+) create mode 100644 Libs/UnityEngine.dll create mode 100644 Libs/websocket-sharp.dll create mode 100644 LiveQuery/LiveQuery.PCL/LiveQuery.PCL.csproj create mode 100644 LiveQuery/LiveQuery.PCL/Properties/AssemblyInfo.cs create mode 100644 LiveQuery/LiveQuery.Test/LiveQuery.Test.csproj create mode 100644 LiveQuery/LiveQuery.Test/Test.cs create mode 100644 LiveQuery/LiveQuery.Test/packages.config create mode 100644 LiveQuery/LiveQuery.Unity/LiveQuery.Unity.csproj create mode 100644 LiveQuery/LiveQuery.Unity/Properties/AssemblyInfo.cs create mode 100644 LiveQuery/Source/AVLiveQuery.cs create mode 100644 LiveQuery/Source/AVLiveQueryEventArgs.cs create mode 100644 LiveQuery/Source/AVLiveQueryExtensions.cs create mode 100644 RTM/RTM.PCL/Properties/AssemblyInfo.cs create mode 100644 RTM/RTM.PCL/RTM.PCL.csproj create mode 100644 RTM/RTM.PCL/packages.config create mode 100644 RTM/RTM.Test/RTM.Test.csproj create mode 100644 RTM/RTM.Test/Test.cs create mode 100644 RTM/RTM.Test/packages.config create mode 100644 RTM/RTM.Unity/Properties/AssemblyInfo.cs create mode 100644 RTM/RTM.Unity/RTM.Unity.csproj create mode 100644 RTM/Source/Internal/AVIMCorePlugins.cs create mode 100644 RTM/Source/Internal/Command/AVIMCommand.cs create mode 100644 RTM/Source/Internal/Command/AVIMCommandRunner.cs create mode 100644 RTM/Source/Internal/Command/AckCommand.cs create mode 100644 RTM/Source/Internal/Command/ConversationCommand.cs create mode 100644 RTM/Source/Internal/Command/IAVIMCommandRunner.cs create mode 100644 RTM/Source/Internal/Command/MessageCommand.cs create mode 100644 RTM/Source/Internal/Command/PatchCommand.cs create mode 100644 RTM/Source/Internal/Command/ReadCommand.cs create mode 100644 RTM/Source/Internal/Command/SessionCommand.cs create mode 100644 RTM/Source/Internal/DataEngine/Controller/DateTimeEngine.cs create mode 100644 RTM/Source/Internal/DataEngine/Controller/DictionaryEngine.cs create mode 100644 RTM/Source/Internal/DataEngine/Controller/StringEngine.cs create mode 100644 RTM/Source/Internal/IAVIMPlatformHooks.cs create mode 100644 RTM/Source/Internal/Message/Subclassing/FreeStyleMessageClassInfo.cs create mode 100644 RTM/Source/Internal/Message/Subclassing/FreeStyleMessageClassingController.cs create mode 100644 RTM/Source/Internal/Message/Subclassing/IFreeStyleMessageClassingController.cs create mode 100644 RTM/Source/Internal/Protocol/AVIMProtocol.cs create mode 100644 RTM/Source/Internal/Router/AVRouterController.cs create mode 100644 RTM/Source/Internal/Router/IAVRouterController.cs create mode 100644 RTM/Source/Internal/Router/State/RouterState.cs create mode 100644 RTM/Source/Internal/Timer/IAVTimer.cs create mode 100644 RTM/Source/Internal/Timer/Portable/AVTimer.Portable.cs create mode 100644 RTM/Source/Internal/Timer/Unity/AVTimer.Unity.cs create mode 100644 RTM/Source/Internal/WebSocket/IWebSocketClient.cs create mode 100644 RTM/Source/Internal/WebSocket/NetCore/DefaultWebSocketClient.NetCore.cs create mode 100644 RTM/Source/Internal/WebSocket/NetFx45/WebSocketClient.NetFx45.cs create mode 100644 RTM/Source/Internal/WebSocket/Portable/DefaultWebSocketClient.Portable.cs create mode 100644 RTM/Source/Internal/WebSocket/Unity/DefaultWebSocketClient.Unity.cs create mode 100644 RTM/Source/Internal/WebSocket/Unity/websocket-sharp.dll create mode 100644 RTM/Source/Public/AVIMAudioMessage.cs create mode 100644 RTM/Source/Public/AVIMBinaryMessage.cs create mode 100644 RTM/Source/Public/AVIMClient.cs create mode 100644 RTM/Source/Public/AVIMConversation.cs create mode 100644 RTM/Source/Public/AVIMConversationQuery.cs create mode 100644 RTM/Source/Public/AVIMEnumerator.cs create mode 100644 RTM/Source/Public/AVIMEventArgs.cs create mode 100644 RTM/Source/Public/AVIMException.cs create mode 100644 RTM/Source/Public/AVIMImageMessage.cs create mode 100644 RTM/Source/Public/AVIMMessage.cs create mode 100644 RTM/Source/Public/AVIMMessageClassNameAttribute.cs create mode 100644 RTM/Source/Public/AVIMMessageFieldNameAttribute.cs create mode 100644 RTM/Source/Public/AVIMMessageListener.cs create mode 100644 RTM/Source/Public/AVIMNotice.cs create mode 100644 RTM/Source/Public/AVIMRecalledMessage.cs create mode 100644 RTM/Source/Public/AVIMSignature.cs create mode 100644 RTM/Source/Public/AVIMTemporaryConversation.cs create mode 100644 RTM/Source/Public/AVIMTextMessage.cs create mode 100644 RTM/Source/Public/AVIMTypedMessage.cs create mode 100644 RTM/Source/Public/AVIMTypedMessageTypeIntAttribute.cs create mode 100644 RTM/Source/Public/AVRealtime.cs create mode 100644 RTM/Source/Public/IAVIMListener.cs create mode 100644 RTM/Source/Public/IAVIMMessage.cs create mode 100644 RTM/Source/Public/ICacheEngine.cs create mode 100644 RTM/Source/Public/ISignatureFactory.cs create mode 100644 RTM/Source/Public/Listener/AVIMConversationListener.cs create mode 100644 RTM/Source/Public/Listener/ConversationUnreadListener.cs create mode 100644 RTM/Source/Public/Listener/GoAwayListener.cs create mode 100644 RTM/Source/Public/Listener/MessagePatchListener.cs create mode 100644 RTM/Source/Public/Listener/OfflineMessageListener.cs create mode 100644 RTM/Source/Public/Listener/SessionListener.cs create mode 100644 RTM/Source/Public/Unity/AVRealtimeBehavior.cs create mode 100644 Storage/Source/Internal/AVCorePlugins.cs create mode 100644 Storage/Source/Internal/AppRouter/AppRouterController.cs create mode 100644 Storage/Source/Internal/AppRouter/AppRouterState.cs create mode 100644 Storage/Source/Internal/AppRouter/IAppRouterController.cs create mode 100644 Storage/Source/Internal/Authentication/IAVAuthenticationProvider.cs create mode 100644 Storage/Source/Internal/Cloud/Controller/AVCloudCodeController.cs create mode 100644 Storage/Source/Internal/Cloud/Controller/IAVCloudCodeController.cs create mode 100644 Storage/Source/Internal/Command/AVCommand.cs create mode 100644 Storage/Source/Internal/Command/AVCommandRunner.cs create mode 100644 Storage/Source/Internal/Command/IAVCommandRunner.cs create mode 100644 Storage/Source/Internal/Config/Controller/AVConfigController.cs create mode 100644 Storage/Source/Internal/Config/Controller/AVCurrentConfigController.cs create mode 100644 Storage/Source/Internal/Config/Controller/IAVConfigController.cs create mode 100644 Storage/Source/Internal/Config/Controller/IAVCurrentConfigController.cs create mode 100644 Storage/Source/Internal/Dispatcher/Unity/UnityDispatcher.cs create mode 100644 Storage/Source/Internal/Encoding/AVDecoder.cs create mode 100644 Storage/Source/Internal/Encoding/AVEncoder.cs create mode 100644 Storage/Source/Internal/Encoding/AVObjectCoder.cs create mode 100644 Storage/Source/Internal/Encoding/NoObjectsEncoder.cs create mode 100644 Storage/Source/Internal/Encoding/PointerOrLocalIdEncoder.cs create mode 100644 Storage/Source/Internal/File/Controller/AVFileController.cs create mode 100644 Storage/Source/Internal/File/Controller/AWSS3FileController.cs create mode 100644 Storage/Source/Internal/File/Controller/IAVFileController.cs create mode 100644 Storage/Source/Internal/File/Controller/QCloudCosFileController.cs create mode 100644 Storage/Source/Internal/File/Controller/QiniuFileController.cs create mode 100644 Storage/Source/Internal/File/Cryptography/MD5/MD5.cs create mode 100644 Storage/Source/Internal/File/Cryptography/SHA1/SHA1CryptoServiceProvider.cs create mode 100644 Storage/Source/Internal/File/State/FileState.cs create mode 100644 Storage/Source/Internal/HttpClient/HttpRequest.cs create mode 100644 Storage/Source/Internal/HttpClient/IHttpClient.cs create mode 100644 Storage/Source/Internal/HttpClient/Portable/HttpClient.Portable.cs create mode 100644 Storage/Source/Internal/HttpClient/Unity/HttpClient.Unity.cs create mode 100644 Storage/Source/Internal/IAVCorePlugins.cs create mode 100644 Storage/Source/Internal/InstallationId/Controller/IInstallationIdController.cs create mode 100644 Storage/Source/Internal/InstallationId/Controller/InstallationIdController.cs create mode 100644 Storage/Source/Internal/Object/Controller/AVObjectController.cs create mode 100644 Storage/Source/Internal/Object/Controller/IAVObjectController.cs create mode 100644 Storage/Source/Internal/Object/Controller/IAVObjectCurrentController.cs create mode 100644 Storage/Source/Internal/Object/State/IObjectState.cs create mode 100644 Storage/Source/Internal/Object/State/MutableObjectState.cs create mode 100644 Storage/Source/Internal/Object/Subclassing/IObjectSubclassingController.cs create mode 100644 Storage/Source/Internal/Object/Subclassing/ObjectSubclassInfo.cs create mode 100644 Storage/Source/Internal/Object/Subclassing/ObjectSubclassingController.cs create mode 100644 Storage/Source/Internal/Operation/AVAddOperation.cs create mode 100644 Storage/Source/Internal/Operation/AVAddUniqueOperation.cs create mode 100644 Storage/Source/Internal/Operation/AVDeleteOperation.cs create mode 100644 Storage/Source/Internal/Operation/AVFieldOperations.cs create mode 100644 Storage/Source/Internal/Operation/AVIncrementOperation.cs create mode 100644 Storage/Source/Internal/Operation/AVRelationOperation.cs create mode 100644 Storage/Source/Internal/Operation/AVRemoveOperation.cs create mode 100644 Storage/Source/Internal/Operation/AVSetOperation.cs create mode 100644 Storage/Source/Internal/Operation/IAVFieldOperation.cs create mode 100644 Storage/Source/Internal/Query/Controller/AVQueryController.cs create mode 100644 Storage/Source/Internal/Query/Controller/IAVQueryController.cs create mode 100644 Storage/Source/Internal/Session/Controller/AVSessionController.cs create mode 100644 Storage/Source/Internal/Session/Controller/IAVSessionController.cs create mode 100644 Storage/Source/Internal/Storage/IStorageController.cs create mode 100644 Storage/Source/Internal/Storage/NetCore/StorageController.cs create mode 100644 Storage/Source/Internal/Storage/Portable/StorageController.cs create mode 100644 Storage/Source/Internal/Storage/Unity/StorageController.cs create mode 100644 Storage/Source/Internal/User/Controller/AVCurrentUserController.cs create mode 100644 Storage/Source/Internal/User/Controller/AVUserController.cs create mode 100644 Storage/Source/Internal/User/Controller/IAVCurrentUserController.cs create mode 100644 Storage/Source/Internal/User/Controller/IAVUserController.cs create mode 100644 Storage/Source/Internal/Utilities/AVConfigExtensions.cs create mode 100644 Storage/Source/Internal/Utilities/AVFileExtensions.cs create mode 100644 Storage/Source/Internal/Utilities/AVObjectExtensions.cs create mode 100644 Storage/Source/Internal/Utilities/AVQueryExtensions.cs create mode 100644 Storage/Source/Internal/Utilities/AVRelationExtensions.cs create mode 100644 Storage/Source/Internal/Utilities/AVSessionExtensions.cs create mode 100644 Storage/Source/Internal/Utilities/AVUserExtensions.cs create mode 100644 Storage/Source/Internal/Utilities/FlexibleDictionaryWrapper.cs create mode 100644 Storage/Source/Internal/Utilities/FlexibleListWrapper.cs create mode 100644 Storage/Source/Internal/Utilities/IJsonConvertible.cs create mode 100644 Storage/Source/Internal/Utilities/IdentityEqualityComparer.cs create mode 100644 Storage/Source/Internal/Utilities/InternalExtensions.cs create mode 100644 Storage/Source/Internal/Utilities/Json.cs create mode 100644 Storage/Source/Internal/Utilities/LockSet.cs create mode 100644 Storage/Source/Internal/Utilities/ReflectionHelpers.cs create mode 100644 Storage/Source/Internal/Utilities/SynchronizedEventHandler.cs create mode 100644 Storage/Source/Internal/Utilities/TaskQueue.cs create mode 100644 Storage/Source/Internal/Utilities/XamarinAttributes.cs create mode 100644 Storage/Source/Public/AVACL.cs create mode 100644 Storage/Source/Public/AVClassNameAttribute.cs create mode 100644 Storage/Source/Public/AVClient.cs create mode 100644 Storage/Source/Public/AVCloud.cs create mode 100644 Storage/Source/Public/AVConfig.cs create mode 100644 Storage/Source/Public/AVDownloadProgressEventArgs.cs create mode 100644 Storage/Source/Public/AVException.cs create mode 100644 Storage/Source/Public/AVExtensions.cs create mode 100644 Storage/Source/Public/AVFieldNameAttribute.cs create mode 100644 Storage/Source/Public/AVFile.cs create mode 100644 Storage/Source/Public/AVGeoDistance.cs create mode 100644 Storage/Source/Public/AVGeoPoint.cs create mode 100644 Storage/Source/Public/AVObject.cs create mode 100644 Storage/Source/Public/AVQuery.cs create mode 100644 Storage/Source/Public/AVQueryExtensions.cs create mode 100644 Storage/Source/Public/AVRelation.cs create mode 100644 Storage/Source/Public/AVRole.cs create mode 100644 Storage/Source/Public/AVSession.cs create mode 100644 Storage/Source/Public/AVStatus.cs create mode 100644 Storage/Source/Public/AVUploadProgressEventArgs.cs create mode 100644 Storage/Source/Public/AVUser.cs create mode 100644 Storage/Source/Public/AVUserAuthDataLogInOption.cs create mode 100644 Storage/Source/Public/IAVQuery.cs create mode 100644 Storage/Source/Public/LeaderBoard/AVLeaderboard.cs create mode 100644 Storage/Source/Public/LeaderBoard/AVLeaderboardArchive.cs create mode 100644 Storage/Source/Public/LeaderBoard/AVRanking.cs create mode 100644 Storage/Source/Public/LeaderBoard/AVStatistic.cs create mode 100644 Storage/Source/Public/Unity/AVInitializeBehaviour.cs create mode 100644 Storage/Source/Public/Utilities/Conversion.cs create mode 100644 Storage/Storage.PCL/Properties/AssemblyInfo.cs create mode 100644 Storage/Storage.PCL/Storage.PCL.csproj create mode 100644 Storage/Storage.PCL/packages.config create mode 100644 Storage/Storage.Test/Storage.Test.csproj create mode 100644 Storage/Storage.Test/Test.cs create mode 100644 Storage/Storage.Test/packages.config create mode 100644 Storage/Storage.Unity/Properties/AssemblyInfo.cs create mode 100644 Storage/Storage.Unity/Storage.Unity.csproj create mode 100644 csharp-sdk.sln diff --git a/Libs/UnityEngine.dll b/Libs/UnityEngine.dll new file mode 100644 index 0000000000000000000000000000000000000000..80cf67e5076e96996396fc4ed3a1e49a80251c4b GIT binary patch literal 1345536 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
r>Efl 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 z≪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�wo2Yygxe;VGQTaIXOvPE1Pr@9n zcueI}@Us=?R6Y%Vtm1K%JK^Ul&Z~R|{&>X`DxZaK0$Eq7+=bi>vRYgD9CC|dYvpd3 zg^DLu?tyOu8NFA&fPAuId*zETixd}EzJ$D_@?~&MEi+lx$wX7Tm zURQZAcvIz};H{O1;ro3MJ61U!`NtqTjFl6Re*!YjubhN@7szP8ax(HgAY+WmDabzs z8C_H!h5R#+b;wE|@-Gx0sSLn8toUeUDg0v~JB*d($WJK#x-tawwBpXnY4E>M{B7lQ z_-7Q~uB=9Wzj6k?|5g03vJU10#i`T7@IDZ0GhKcKwM4OE`b?OR;CG^Gif2x5fnN)rW>ZF(DW5@duq{bca` z>FwZ))0e>i8Dumy{S@TCfXp(ccObtF^2LnlE0Euvz6$vr#rLMKhS>*VrKg_`elYzE z@WbhAz>lU!L940@990zqi>i9T;;J|}x@s+9$AH-CssZG&AnQL>N#w&o_8+UxLLLt? z|E)@cM^+7kQ>xa3pQ;)G%c{-+%d5@BP}L@IM%8Apw(26VuIdu7zUoqNR@LR; zF;!cLc{WIEt-1p2sJaqdUbPimQMC<>S6vOJs;&VyR9y>RUUeP#g{tepEmb!V!xt65 zQgtKDHpQ!}Zi4?R$oRSHX5_Cae!c1zm}?cUtGX5b8;akox()t%#UEGQ4u7ZOj;ihO zKT*8D>JIn^K=#+F?nHi2@u8|6Fh5uPMb+K#4=X-ebuauQAZHY-?n8b|@rkMjV4hTb zs_H@bUn=gbdI_jiu^mp-&Z{jvrF;0swd(9p!mnCr{H%h?x}hj z{&|p@Z`DrZmq145RnH*5toTaRvoL>E{9Dy7_*WHQt9lOp?}~3!?S_9-@n2PY;NMbw zyXpn_e=ELI^&J|9^Dvqjt72Z-DQ~etJ0g4A!zYafE z@sR2_;15+itokkZPbf~Pej9$G;t|z*;U_6hu6`H(NRT>Ny$|_Qic_oKhdD~oSN#FJ zU$Lb6Blv(~Y4s?6owiJIT6OUx*1;4jtH;1kSFElc3ty#JQ#}rT2FOgg`e5V+#Ypv` zFpY{cs}F-eTJf0b@$j=0XID>vKUVR$>Phf(6z5h?hCg2Mgz72q^Aww^kAj~MGN;ZTw7fYeY1|B~X? z>UQ`qEB>T<3G$BWQ^3DfcYq&OuRyc7W)*S~h{dZ}jXYZMfSS``#)7Pk*PMYoPVwNH zH82M$9#Rv9KUDFsnlAWHC>~xDgC7qvvaRVwo&YkAs)-{{0$HD_S&KXw#B$aQAWu;| zswN5ZDaENZXTkeGc9d$;$N`WQo0?(dQV<(kvmUuzF;p`G6I86IIR}0kNIz9`9#~zo z5v;A*1lHAT2J34s0>d?zfQ>bmq8|a78`fNoe6->*HCte2DbA_60{%F~<7=*jpQ|{p zW-I&&ATz<5ZOHQ#7t~x0)2!HDa}Dyknrrc0uXs+)buecuo?CN0{COZ_>6#mm?^k@V z=0=#GDgM0XCisUGAFjC>{udx87HV!menjyPHMhd-R(zr6Hu&ci|5S53{EHyFEj8Q0 z*K6(o->kV4e5+;$zW)N*PpP>Ze6QwS@Q4}r!A}M$qZto?r_Oi~JblJP;HPIijPDsB z`{pwq1-oWE4jx?lBv@Aa6xdYzG`OI4Cz>rFV}sgfkXseoYM+HUQSqeOUGOI>w%0xf zzesU;?QZ0iwR^x+?F-;#wJ(C-u6+r-r}kw+-K%(C?JF=pRlL9URrm)Kch$ZI|2xIs z*S?PYT1{s0WzK{GDkh2`M zAAo4l2?yWr-e5dwMa9{0V;D2hzgYVZ)0RLM% z3H+dTGWcQb6!4?kqrgMzd|pnK12&OX_C9F9q3At(%R!3}j4F zHwSq+$e5&VF7isnQ|soztWsQE*93o>;-~AH;ZIjQqpk)1Ga%#tx>n>)#h$u0n3!U` z?qv8gLFNT@?a2Kg@0ZmrK~5+R)|~>AR6MJ$13m>7S?RhJ;CXecV9o~_`PZ#R-V8GG zuR9(2B9PI3-5JQ2DsHJ;1M>yNFV;ojuK=;1bzR6?6|b&~!F&Z|4WzCY`KyXwtBb>2 z3o;|9TZ{Y+#c$RPz+A6*LtPU7TOg~Eb!Q=eTk$(}X_%W7Z>}4L|1QWbZQXk0?}5xL z>PC>i53-6J;cbN!9r=jv|)FRH&8ytw`r@RIsl!Oz#<23}f!J9t_BcJT80 zJHRj0-wAH1-vNH9{%-DarQ)6S_rm;G@hA27!S7JKyZ!XM*Y+9Zz{f1zY~71;=A?F zz`qC5mg}EI{!q~h?}GWLUa&a)9Qzm8n1SRZ}^CJeGB5Pl1648ILV!h6AE!tdfc8)P;S-UrSJzYiW4 z{s5dC{s=riJZduIt8npT_KU+~klPeb4v&Rd1Y!-sjJw^B^usiGnW8nbU6D|e&!sXyq;SieJK-yY( z8hAr^I?T5~EPuEf+!3Au-W9F`?+%B-d%}(2z2TYQPs6jo`@*xq`@?gK$WMe%2A>SKgTD+f0iOz=0{$x8f&R|$ z3glla{wBN%<{6N8sluzlXTzt1zYCuM?h3B~cZZ|kAH!W}?g8m>!ZG9*Kz0qny~rAmo(f0b6LZ!$d@W!-f$c87aDGd z-vY9#)36=+3J?p^a0l|0id!4*g!!`K{S7;iA85E6-v=A+MgE!M&l~Q8`9;G6Fb^p{ z+3+Cpo`#3OQyU+~cQr^)*Z3&d*Z4RXZ+sFwxA7_PyvC=&^BZ@98ylYiFKB!g+|;;> zuopKzhkTLZm5sZRztXq|yt?rP@N11Pg4Z^_1m4#8GWg5JR|xg5##fQw0-2LGzJ~lZ z$TvkBUq{}n_+H~1F#ABpA&qY#{}*Hg()c#=M<8?3$X?`8ip7z4VTwTJr;&ZgV-ydJ zybm)@@!-e@@CSkHB1JwzK2-7W$fzT!T_7`pNHOvR#YvGdFcU%Ms*$nClNF~##=(3_ zacbmX_@fm4kwcLyB8P#~BICiz$OLeDWD-~vnG9A(rhqk(qre#vA6Od+fc245@aRZ6 zI4cq&{$mu6jZA}?tvDw#9sW2F+Zd?^mqliPDqggFyrwiua( z+^?93%tk&pG6y^_G8eo!G7r2Y(gc1!(hOc2X#u|wX$7}L+Q2VHP6n@tw1Z!YECH{K zoC1D1(gALbtN_0rSp{AfSq*+8ayoc@+E9_@l@$%pD;8Y-BxnXJiEYN#q>xuE=@# z-VI`VBO8(LRlF~<3FfDY_eVCvKLFBRA{QY)sQ6Ii5}2PW{vvWI{KFs?IC44iql%A5 zw!l25_(bFi_$NVXc;rg(smNBCUxC=($To0iK8?-l<<+dr-j-vimql$MFB~}#|?<{&}b*%Ug zMZY{PR{U7;Z#qBI^;q$5J3kBluCo*TOJ_Iu*Up} zvxi@4pF7^N<{f2OBSFi$0#`KMvc}@RceG_~$NdubgVQW)Va&2B&$6t==kx3K7g$yw zZsR7)x&*g;vt@OD(XyWX60gc$X<4V^wtd;M7W~Mv{_?bCy^EXmo@KS;=I^ttm3$jp zzT7^Q@5{(nwQ6zl#jDeB-MCYEFJc0Fm{W0bqM{Zjdl`>aaKdRCUrnf_T&fr+;U-q| zj&mI|_6D*2j974k5oRPanQ`D&<9cvu+&v?rW%1pB-aOJbY?&j5X=-(bVtU zKbngE(Z9KWoHnw2?v-T~TVvcS@vCsJXj>jx{2Dyh2U`bOhv0%%xfQ}y;KEkDb&NI3 zy1?3KZNgoM+l;%y`l9tE+%493t=p{cTR*_vj=R^o$NGcyob?}TpY@UTp=FKw&>Dp+ z!WEAy8Z~CrXg=d~(5ORjpBQ!MsKa@cXguzdxCyuuM$H@5gqx3R#x20L;1=RqaVO&1 za3|qT#x26N;}+wV;FjV}!7ame;FjZ7;8x;R;ZDV^#+`;c9rtP68Mr4$Ju&Lnqn;kM z6Zh{?Z;$$D)Q6)YMU6%Ci<*ksicTzQFIrTzyr`pS1#Ts774B5rYTRdv&L~=g`z$Vs z>%?^zbrtm&tt}cXN*0}0bZ*i4xQ)2a6>ToM2=}9+?L|K>x})e$+)r>jaK9>gs_1t` z&lc^%{T}xm?hm-#xIg0d;GW05fctaNi$yQt{(^fM_gCC2xWD0E#r+-k8txys*Kz;E zy@7iZ_ZIG*qP<1$;`SB2SM=Ya_lrKjeTW-VJi7P*+*lkx6IVPAcM$Gi+#$G66dziA z818V~c-$v(6L1r8WyPh%<+vbjT5&~jRq^!V$BQ2;e!BQq#o^KQqZ@FIxCm}0Zr13d zM<0VbcJ%Dgb8yGu=Hlj!K7Mp`^k+wRjqV)Xjf>%WNB4|gH+p#VdYn{QpY`>}i?!|s zT&dKR%3W!?^&MNU+Lh+G(p*<>o+~xGzAY}->PnYb+wJ%+b@eWHeYd#WxZ($HHEnHz zD@}H#fGbUNr8-xdu0fbBq&nze>`I`^P+}cRSoljJcQe36~i2n4Quejd{`L zel+H;!)r8ZY;ccoKYX_YIjcBRu@=?quua;2Cn^}5n+u5|x_PW`+8 zK&SrQI?>7T+gxe8E8XczJ6!2*SGw1gUUQ|lU1_f?)lG5k(dbIETxqT=wYXB7E1lv> z>jO?KBd&CgE1l;`8(nFWD{Z!=BY7{z#B#WGg)JRv9a`$dJg(W14t1r&Z0SgA=0f9p zq?L4~TU=?kD@|^7^qO61n=M(^-OHVOl&>(-;a1d@maKB@#*$Tz-6%fI2{Fc%wy$yQ z-;OnoWxd;#9(ASbKkI~@8FQowJ&rWVmCosPxbs|TqbqH8rHfqY5?dN=UG7R-TACq3%(gyOh66 zWB$9w@ORbuyXyQ)`F|__Z{`23{4ob^h%y(mHZyyxKj1#_{D7qJ!V6qxiPsxW&Q*6lW@)tQb{X zuee3=M#Z}opH_TX@qO@^15Z7L9Tvs5(Oa2aojm$xux<36D>cv#wet7&=w(Ot4bx zWTmz1bdjazR9d@L{)gh5ivL!W7%HvzRQ@mcjnA%RK30FgEzG30S#J&tRx@*o#LV-q zQ~5%0oAq0j|EzL5v#Chz3~*wPcG2W&i)FsFvSlP#~cu2el_+0;SWAwGxB(Au=ta>3AjnP$+#)j z5M#^kgFa(@(^|`QKW+e*z$I~mxU+C6TpBlo8^*1}t;cP^jaa8xXYu-mh zhdUp)k?RX^n{XH6Hsd~ry9jqNzoq(l>(d8a%Dt~-?zEM7uELQSD-!Xck(NYnJQ1ty z?(g?rTLbB?WU4>jX?2XGhhhV@&B;{EmaXI3likDpvE!|#M0Y9~?`}&B4i8yNI?s%C z4Oz8aL&=oY8XM|}btMzs(bUNDHdt+om$xii+_Y#-bJL4I42aPQJoJTa8E2G(?|H_0DZb7ssou1;G@6PIG^J9}k*1-cRJ?O|C}y=KhL#SctkYttq?H_8 z({k2uw4V~TS~hgW28ZIwgyr1Dq$_4MM=AD!*ic`xTceiij-lvKyh}>Z>WRntjYkJp zl#Q0F@rTK1E{m?WADLiWm?UQ&mg>}Nb&$?P@BHC-e|Ie9)T`yO4MSGb;9xA#Es-xD z8H`y5l1ASSW;G`hUC|+{OH!PU_a>r4!_>$%`I){jp5U?1j#(ymxuw*fOeKd>|%?u^IBWvB8?3O!mj332Xj{$@1{glJQ4J ztPXBPxCO~!DevaKXv)$8vZS6{Ya|QxUUs%=?KgQTem0x;JPTs|v7y+aBoDqU*5foe zsT-QfIo@{Vk+?HuVqcj;Sr!}Yk9Nf@GNRis38_@QG`Z{wSKFm6Ih*t(&jj`sfML!w zZFr@$WG%_Ftxl`UN?UR3OvWtTR=@iYi`!0&^$$`R1bPE zo#{NYRFrnDw6O7XrW!7k`U=;aibaQFl5z@B+nsf0%8@djOH;`WBi5i|wqeTEAtCEx zsiEb`lNPs`GL1^pb8aMLui;I3sG7~3nTX??)~4guTUxu+Xo*y+oExpl1Sgjlw^_9A zD0Xl-m5L>XmiO^s-PVFwK1Y_UjHS}j$g>|MQ_gZDs!%^xI@WFS&q~F5Y$+#H0o|>k zO=-0(G+%jEOH&v&{~9bZKe^uE;@El;o=!)5V;SSRq^nCxsXON$uJLvKU1mWn-G%A) z#L2SJF+x|k#eGKb280`|1zBD=Lp$D=0*-w`3PkLMXlbyzb zWGiNQnaZ0{#nUxBdBI$t923-*?jWglcR)Q4TR?-=l1O8)HBhtj6ixm8$u3(nJASVZ zsVJzE;VDO>VJuK~RJoq%HjGL%(G^?LBOOUj#-Z(+mb`ehMp0L#-l*l?ziN)Z8^Vbq zFK!dS@#fh~_qSsCLaURWCb#NlDQN7{xuU%&mgr^RgWWXAv}x&%(%Rpfcptss$>Dww>H+ zVryqt7DMJ#8AIk)9K+^SA4BItvHMYUeU>LB{e`tMKFgE(NQKorUdxj_W??PY zV|h{@ys%QnXL(ZdqOh9BYkAV-Phmx`-}0oX(nd2XW7$GoXSSM*iL$a>2{-rTDw9ic z<;e`Wbe~!-GEXO0o`;Z2Z&Kyjn^@|!rrPf3=m)dXl$M!ac3sF-k!v^A`KqoLdYR|R z)600F=XugRMUNkvZYJdExjtxRawktM)oeCGO$J{UtL z)+N`*q`&TrcCBSj+H5av*sFZ@nnx`uWD#kwBbHhh?_%s3XUJ^zcJ-N+kwIB|Vo`zt z<>J`T`ebVDvKW)pXlL9kHzkG#WU**Cwq}jBI5}i{%94sv>yNFBN1L%a@mOjBa~(U9 zEN`7CG%?+EJXs^^kFx?KGu$PKrf!)!$J0DjTQ_ws-a}l@bFp@|DB0^=nI}vYB+Z1H zIa_+ovRJR=R>~7{L5w9MZYguR^vYyEH|HU8vZ^^bFbJJ;^Xio0_z(kaCK&xHu1GM# zH7k_y^ddykyePIVW-q4khyyG{^y=bNGNnHAM|3r=U$j)BdNMF6plPX2o35?)vUHMG ztys6zPZ{nfOt4tDiI@3Tv|D`b)xN^sx*QWvFYS+xkXHMur8_PQyK<9wdR44beC?GR z7Lgh~e)gWhsYsGnZu!C<}NUU8z_sk?u>%Vq-sRq)c+tGF|TwJ1#e4 z-KH%upW51+VlkMKD!`dU*(o|PnkGLNQEwCJn8g)!MM|UFp^WzY=ulVRK(gE1C7xa$ zOAk@~?PoY~cp#b}%|oeVe?OV9BCRnkYh5YUP1m*-CR0svy#Os{@@S4MNl5-$1Ia{^ z<#2gk1|9D5tlYPo>b^AUEKF-*5j*P+v7xlf^Z03PxkJ)AT$*M0!DNaB^=2Ad%%#(* zE=`Z7JSul(EqKMUMb_fsfzDWpWDZ1!+7dlUr{Ez3i+v9z@U zBfxT}{9}o;dAMK7OXHCT!IDH%@n~Ym_Rza~?`XA|J5v_x;ba>yPzf_Uvi4q-K4rvm?g(mTnkjwp={) ziOKZPid4U~gr$6`eZ$C%Nohe+)-i zW|KuGNfWE41D$MGS<&w9HT_9e26P#2S*$A-Ux%Sy78^*ei?J{%{|YG-4(E0R`wi%7 zNNx0V5*CTDYb^DmYwdEfJ7x{ZK1N)7+wLWa_Gmm|!drCKp1=!YJv>V{Id1;zgS)Yy zsMSSGUM6mbZPj_4eM5)rw}WsS=5slonK;~9D8m3_SnZyPy{V*>vX;yKeI=oBZ9i-@ zcsqab=#3?YrHDLob6>1$ZPU7Fyk9rN>^it29ZNM0v-^~CtP$oqpOh^Nb`kSD>{uvl z{H4faV(`R3?@v`)Bc{u=`~Rh==}!-L4#bC=utFoO4jAjgmP221Jx!{Eo-f^J`Y>u< z-T=Zf1Ey3VKes*&>7qWBK(b4_^NeNe*X~;TZsVP~R>L8MW_(?#shf_PreI|lZPb}d zTfww-GdeMGr0Fs<{fgU_(2#QmE_8N`MVwJaW~2~L+oJ^U2!T8@1A%xtXJimh=Z*?; zJ9`r~Wg1Us`s++z9ZwhNr`;~rq%ymEEzq?(cCDFe&j7PO(_z~qJx9SR&;h%hubW$9 z`n=tT({*Hb8+l!Zx4ZCm72a;b+ePGb4<1A8*m)1-*ydb2n`2`$_Q5eH`!@jlY5Y#Z z*Z(<KUNQw90DJCbcY+#JSiR2d>sRseN)FS zi$P=4&GEF@aTn_^GbYBKX11txByD-sJn`klq*jo;R2@5*mAUo=Rr{xFOUx^bt>dnJ zR#&RaO7`@Ssw@rnqMeaLYQzL!puuoE$4iqiK##6B(<|A7HWsl%OsF>%GKWAnt&u30 zsKp#NF|5`9l1B{J`(=ke#zt>f%-tj82!R>cW*9d}rYCbLPvjD*UKUVW6(8#B81A%l z(G8tN|F2*Ft~=6U(s9k;xD7i*=jjjI;%zNt70^8|Z}y&2pIyG2f*S z*u;?qMGD=n+tkwaTJQ13S8HYFzW|~pfv)Yv8?@BY}(g2xu#h9tj+kn(Q<}Gh|Zma>)vHl*3XhEDc z4y>J=z@g^F-Hgtnw3ry$r_7{rV2vC!at2cN?ALAz%!kd;kFHBsHI&ENWX6^5g<1$U zf!CXM!vt@gxdC-|O|+0lJgxD&al26!)HXf1PFjcAe;#E1%xX@~OiipKcV#%IQw~jT z4!1Bm5NC)f$>q+hh^3P$cMR<^3sW++N_6EYW`=X7zcm%fUEt_VMF;yBTJDF&fU}gL zWh%FKv<GnC$pIGHz$*+ZZq!9zJ+&w?D%x_ z(R|GsR@VKO#y7-rru)Qq5~`!+KX4Nid6BG{ao3h+mq7muB0U-SuOD1Yncg@Y##264MYdC{MrVh zy|I>_9)e{DYFtq8E)5HGDY&4Y9e=(;S8{N~4$)yUEh`OT#8~h#IiH;h9Tm`|bF!g> z_1ko;P$FX8_S5PdJyUO`QY>R2L9p)A<`470W+BkY*bY{iyW;(N$c`3rk$WeS6zeGCVQJV8io(-CFTEqe`2~X|cx3Z4>6w7;$gWHFdED%R08MRcB>d>qsie z0axbeUVT|}mTF)xBW16P9m)6FdCRwmKu8PKoFiFh#nTg2>ZAVJmjIFSc{;Ye&AWID~ z%cfcq-Aj7RDZS?5^iXnOwVqF8VkQSzWlq=D!p4|1Z@b4{8;cD}V8=e&hdixzRxTII zv1n$J%nQ6~j*0|Z)hAfl#MZt^s+?$*)RuM9YWMrusPPxswulC+)4J}IAfTm zTXzVSuUHM2iJ2L8yTiIQX6&2i-=Tv>S&hy*%#S=y-UYnoGs5UhMpNB7*v)+qwbSeMwE!7j8cA-_ zXI=>q(iVm7Ob$_sW*yXsnjq=&Gd&}H;!tcu=33u!$Xv4X5@VNyEe9fWFPtfz%=j|X zy4Ki`+6(#(d(0&}GnA(pmhuLMyEjwYD!aNI(vM4OG6C|Ua7O4>zi#s6-GMU8zNZsF zH-(5!Oswtz%Y&-mAsuB|)OHm;vg7YW#x|u!A-mDCt1QViqN6D>Ia(Rf@zr?RqibXB zcD6EXq`w@JISW0YBX%)cOhqkIzIK7yg{k>vipN`WB*-a5w~*YrWQ@B#;$hLk)G$*o z+^Y<)n{zw^FS~TgChQ8vOjc~eAk!?_knQS|=I$(d^O8bRrj16yOnu}Ga6)6TR1}-p zZLVca%FJ)gwJ8hPnUZE@ICiSdW|dxcvjolTFuSGZspW}eVAuvpYqj5>uf&-aJvkaZ7`KYWuA#;Lun7m1U&`2wf#mi)mmgW?(7T6ho0m*%~AO; z*9ke1F8QW{YS=^%|Mglrx+Og;%0{qaHe=jnf(s4|QlXNJ&7qNxla z8Yvx+(Rg&@TU4?c(TR2q@ye5V7)Mk1zHajf^1n4UbmD?#tD>m{8fL!4l%-GRpE3>C zQMtsPM@mFqQlBJ?P>sPT^OX*GU5wS3vMJGGPc}hG@&sKYS-J&1Bt*B&YW0>z5-yq^ zWag=)EehL35%Xy6(ezsN@?MKSwO}1ZB{c!HMeZpM=69@wIiWIF5+<_`Krh7bMII}c zb4^E~0Ip}DkP<)^p)^_*Ty@vWWh4UC5wAg6S6Jo*&0M+K*-9=gkz}+*$!v|-b2d## zHq*hoX*w%5)8BL(>kC$wW`-${T!r>!%T*_rH-YR)v;FeE&Hkmi+3Y4ZQ$%?>8nVe4p91=}k80`Za=ULR zpz8IOrFzU3M@$Ya>NHG388r`?(PYDh zp&>^Y?BR=1^|G1@ZAm(j(NttEGcmQtb(2sIzGUv{AzS5)h9p;dPOatkpg4!!V8upq z&ot+U-C#ya8lkaNEriP4IKy=myl;Q32kWmUB_(f2+p=2_S|17r&1W~T8Io z^w~^t7N%H87#?7%&TKDjNv|y%7#1^mhue>?Gh@n0$^X*UCR;FHoV1!wQC^*PM&8+s zZS~EOn5D@$r@?sv-t1{-=Sl%ho08@r&uM}e43(|oT-hCCp>PFNOe#MnQ~?Etaw05r zv%)$~2;Lv*l0&bBLKfC>SPj8@u*`X~bESZS=N^SZTtLC(nw&(&Q_p>nLBoU2av|kO=IEY`yN}gn?`QRpt2*j;Yz8Oc zvox12&n%?ncsavmc|JW3F3H9?=JMCNK|=w`ORWOc(z4xy=4 zQb*19z1>+>9;ulwg9Qwl>`pcH&^IoUu8l#{(x`l5LrUypHQM-IM89;YoX$&fc&3}( zHV%e&+e7x)x*@C6Y=@b1r`g^GxHRP;CsUhfRoWCF@pSLnriV<|0F=EYR$9HZfhCnGlGzKEaY<@O&-I{Rf?&aR$a?&RLB z*4q8-0OJs81Y9{AeVmVYRJ-OObGi`D&J<}$! zYsNxxSYOuEQtU>qvvfg27BRdF7D8qg9`Mavq_S2Z3avHB`T`wS_F{sUV1F-bd7$8Q z-=51e25{%`h34#;8G7zaT&KsG>8`@lQ};VG<7C@A-X-JAMU42Z zW%@OwcHUT7B8HOLNJc?eMx(NNNK-cF7j^w-1zSpTMtngME3{Nj6P!3SfI*X`p*|!| z!^yuFQ=e*f~M1TfsP*7;V>b(5U(H%GJ73TQLwao#ZMB}MjD6|Fs^>AhHph17*Y z4kIlbrb%+{gTvVLj-0tPugl=011esT$tW!6D_zcc-;QVxFE8XC3ijTmEz!&-TsMY+ zv;GVmx_LiFY%Qm+y=rNXUP@Eon0xb#Z3*&_VN7nILTt7U*(sYMPS=fnjHbH!bXsa| zq+2Gv^m9WDnxwyz|B_|gP{vVm*@0cN&(E$HT+i-V8Z#vwM%wFTagHw9Ej^E|YY_(} zL7kD8{5s4Ad8|9Lp(VGa6N>ZN3oGkcS9CLSZQ7}d{aO!&#nm3)UZN;hg|^a4%<{9`n@IOg+tHgDY_Z^URs&Wz*U7tfsr+CiXMh&zJRV zp64>&@=UBsn&)u@ZsmE7+-GpE?et6KaL*IzSE5elEh&yia%#|)T4O^-GKaHT*-0Lh zb@McP)uL*4zcyI9ouY5CS_$%kLn4eysjKo&-m8`W=Ezmf&?LhnX#~uE<%LptgOz;e zZ4CKjmhFQCo9(3p9!{?}WDc{*9c@a+l;%x1t5?5`WAo-zQ7&WBZV&rp`YUe_TISUa zeg5>RSy!vGu1=Gq&l-fR(esx25jQ(JW6MM%$*3sPepqf!FEJ;T-KWlr$1W~AsxIw} z(wJWso!WQ}!R9-MdpHRWpB%9n`xt;-=cNmix@?D}fbI68ri5)kW?>W;Wz0u&$U}KA z(UKP}-12Zpb|?pQ4+TQI##qXjTTPmC!3{JD=ee9~8SI-5l@}|lZqCNy>^wkcF?L~H z%shb}K+QUUP(VT7J!7)R7z_(-YL2-~%UeUxyph|r){1vq@~t12XUdC7OqU!-&=o>1MH)5X1HU=G6Y+|Kd8H{+O`X@P^L<)mL&@>x1%m}< zt6Q?io|xK9lvnxkgOe8XLe+|vW@&9?QNEJigYy(-al&mo_VB>mJUA z3y*_3sL7tN6WM;WmnZG9GEc}+ENR(k%bwt9b+ls2nb!g^T6}g;zrvYX$UC1hD#^+4 ztenj*3Rw|i6?{0wA$wik;j~QByi44X>=|N&>iCN7R*l8-&VgJ})=UJ=%d{p2DH2?{ZcEmJy&8>sJ&5^qTa!L(t<@EZEXEXS?{qHq@wE=$w9v~Sv<6iS&LPxL@sjDgrr~~@08ATu;KhR`x z0yB$r8oA`%{``oX!yJAK_!DU3`Ty+~ZZRt|J!zL59lr|?;S9QD$)({olxeZeal4d;Gt#as9Ca1 zJ7W4YX6EwY3d!^JR-btd(@wVY-42GV`n2uzD6u9P&@oZb3q3E1!I5vCa2kY)JLKev zPJm@QA~z4TO-hqhlc5u0+M)Ir`fY*yTuYg63gmlG;ATwMvVr52oC|?)AFL2(vd5eu zAv;{%OeSbFPWIVJcaD>o+|#R^EKx4C!IGt6Cu2>~xtDgyd5blxNOQ8ueG67iKAWKC zTOO-e7U3+J9CKC=iw$``?x|;qS~JsLh7M+hgQ2h%3ZpA#&bjL&_UCBGH5o8t%IRDj zlPN1}9qy_|Q(Ak@Wo&Mw6818OSape!v4*^$Vz1IWD=TKqZw#a+PwbA`7Uk^K8X;|w z^3Z%Y*2|C`@~)gT8gto}IFYXn$x4gPaE&_k&Lo8Q#4x>7bO~U_VVu;JJGd8KAAPS~ zKZ-z?*`3Ke*#eUl8ezS1MkDRePF}4s8x-cz)l*hL-9pg(@~UJyAyYb;sP(S;0QyaU zOm`tEH23eq%w<;NfF~Q6apgIz1 z>xwpe;X^))!?v`Zregt)aY_$k*_6zaShVVMWXjZL&6#TZBL5bin5EWeKcUUQNN#|P zr%Op=Fg5gBq=4%yT{*cTNtDuPSvICMUru|C?l9rGqEze$m4lp1MH`_we#a2`GJPRo0A!l~dU(e-i}SI14kQkF!RU5hF& z3DE2uL9a~j+YuX*ok)|@J&bfkN&ZVH{%i5toXR$z$zGJqg`1Kr6_>RmF}k+O0=`w| zd~}C+oJ-r6$wtbZm^lf{CT-1@L^mC{SuC*c@&*B<|lwJgLrM}2J_ zmvf(`Pyp9c5|A6x6F`2Rg5|KhyJYchWcv%~DtPXe6@w?BOjP;NgWSjR(r$G73Fi8H zZs!f}38qI4w6yF@FoYXJo~rBPiQ9zns7ha-sVjEO8PcQ4V2w`86PF>}yX2|5K6zn8 zl_9+SX}HYYM6nQ`tuJdNvci$A?XaTD;&PJBK>N$(xrAu+$oMi#C7W?|7V-jm$6yTm zE4Ndn3?*9WwIh6nO?_=qS-$)y0n+vt0Qn$Sl$}D>Ozekt+e={|i6hS^Go07BKh9sm zcj6r7 zW@TSzwLqKmHWQmNwcEbPR4bb>^|es_%5Y9C%Bn%xRU@YkIOR`4=M{)PQXcCm#=O$7 zlZs``uA_Y8bfjeieU!{-%-W>sk(gW1Jcdj#si;{* zTSDf%>6r0epfh0SN@|uK^w7+@8ZdAXDA!x7A!u^qh@j`-s^5;ci&HWnmu)lV$;z^$XoZ zx9eFJf$4ku8CHA;dC?m@syD6yPD~RoP9KF`Md;Y4k=5H zeXN4SKyI*hKg9B=BU1TrP%Aw+{F#Kg{wSUTjb zKpl;m3A5VU49N};>je5Dvq?`Dp|_LI0InG9=M;x*adz-EWqWu~$Rj6&S+UZMU9D4@ zARZrMWVh6>KF=tH`bW zh3j`t-KJi9s_cHN>&I5M+)CxCL0PpYzoO(-lC1g=&6HoJE_g^!q1yIGR_0RXWm>z- z;%AE3NSM#50!x-2jM32wN|P|JDykqgiGraXi<)C&-OG#*w~#NpI$sK47f%8>mz==h za}2|shLi0v_BrKss2=BB80#n*Pc^K$MCNRc3^wE#Nvr(a52=dl)U`{_sANSmpVdgy zs<@pI>Rl=9N)4{mh$Mk@HAht43(NCm69~Gd3#Gt;;^luO7bYFWRj&U`$c5; zLVP5B&LrHCg&)@Me5~q=C1hZsi)5O)g?Q0V+o{R=q?FdjLLL%=ZHQ>UZY7)4davD~ zLaw6gAyIon%$F}DNTrq#*a04Ene))C>`zNi(a{$jbSid70nX%{n>|i;6nf->kLyiO zP7)T%RF1=^iL{XZvmB?q81sZ$nuJn!L2Sep~EU)Hnvn42j- zZ9iFz=r|p=PMflH3;THEm34RTEX28=m(MOD`}PGDyy^2Q$Q=hfM#esh#>RqMnjhnx z8`{opPkbgTbD!3m0!i7&`DZ=j|4H$G#VDy}M^FpJGv3dk+_F%W75Y1kZZ3MbY&|?S zjPNrbe57d!TZwvxGCNcu4T)n=mfD&UU3|z~jhOdZ{4?tn#LyYbB&eWA2OH$(Yc?bE zY1?aMJ2{(RxM!c&WM!Mys~fL+a8=UF8JBEQlHes}kdse&w3g_uk4MK4#(*y7S1?IY zb3dn5<-nn{LZ*+F*|*)lirPA-Ni64}TcX!9V=w3-X;Tlqi6^J?$&4k=vFjNR)9edb z;Q|)6fOW;VlK{I8 zkPcasqTNXqF>g+B%7l+HO6~Pti$6cV%lS%eEymns5u!l~+_%l#*=`=`jW;WbJWaDO zXvu*rdm^Q_Syr;;067oG&t0cDB9}Edk#!Ay02L%%3_4On<|v0Y%gm+t%GBTeu9)#SLyj^79%)&wvf*{y3xTrMGqxf zY%hySY=gU&DM`;~MHemk#ID<2d$09GojJs3%GNDcPa&%4DJ)M3xS5rmEm=8`|D3{j z9w zm-loD-noSt-(d3ey8~t^ElsIpGg%4D_HstD7$N!goZVaNYZuyYFOaIo8y9*ZALqETZn2jvxcM|q8C%c6plInJ+b z<#hC>TlTEk*>7(1YFK-8gAndJfnFC^Hhu3yVrPCnxr! zW3K?ZPe#aw|3k>OvzgDUCw1@6Q}Hwa zTk-Mb_{#fX&OLcU%z3F3v7OV0yesA~h{E7;s=#Ui(~Tn7XJFAeRl=2f^#0LBP8tAZ@oERgirEcH(;4!h1W zuChrM{KXy_53ja79-qNT4(LJP<+N0G{)VILp(ld;= zEIg0v1lHJ`3loz*KFe78?%%)~y<`t!0WW$;Iy77GHariK!+3Qhj8`K!jx5it*cbA# z4R1)f_Qovxd07fdWT(x!lDj&xD@cS6sbLq{lU#m!tefKzy-|LgV*v}|ypSvA7>kMA z?pgH8Sw!QV4}N?jX*8I|QHL<WdBDk2E}25y7FSnG-f+>R zX_0DxYQCYUlPK&{l}&5=qT5uDP4jpn8CTVD-O6>{D^m@9%g=T~qOq*mP*m3CJuLHz z7RhNwIH!|EqRvUiTADp?!v>$Dgz-I%o2CUf{)mUDxCO^V`?2x}}I# z&b%{D?4}bp%SPT&8la{q+1yBhc_8ldgpi=f^aQ-cOVH%h&7010BrVOW&=vDzoV+o^ z@came2525h#P1Y|$9%IinHOyoOA=89MWy|(+21d8rEyD(gjvlfVqc68n;kdoU+I}O zr7;$_BI&vCsOfxn$pM^sVCfmO*u8Pj)FTI}=g5v$Y@S_WJh0WFk=J42celJlAS(de z3K~HJ5XDi`78WOqi^nLzy4|(!jW)j=G_abhYMZet#Ph|lv8sBl(YfGOr=u9JlDha! zN!wDE4l$Y574%-Wj0_U@_z_g8fhR|AC+BC!2k(#GzMkfs zhpg^2cUnlV8gf-~Hb0U2)V@yTnVE&Ylo-NPVcYoVc8T9B(FSUgJQ#Kww$uatxz>S3Ow79az6tJeLq!;WeV^3UV_y?<*E7_u1sY*z=9S)XkneJURhlE?o*R7uLwrfHv zV+%pJ?cJw)%?RvaB<0^<8s@a0nKSOlW_S*HBZ7t<#&XM+4TjJ1`|Aq@j z-YoE~Efb2*D#yFs1MkN_@ZP51Iix49?m0H-5sCYxUZ5Y&IM74IU0S@xeUy^6z5gAvAN8*vJ55RD z&URGpSO9ybpuBVgE4RZsE^n7*e(`t7T;NpWRT5gq748E@yU?5RF;tyE!8%a zewp00V60_Z|LOgs?zmOo5w#-%RG`@a~;`A z7h1Tk=eSpsNfkXu<(68~8N6BI8;FBdUuYyPamOkV9VNGerYmbR(Bis#aQD%-tz7Y~ zvYDkM8lGfAtMSu=nR!Q+;bK)q7ukhh+!H1~D}sQKF*^#w``W{0ykgCjE_*;HKmMtU z?RI{$-n#lS>6PT{$!Tq`tQq%m&AQChmUq(FFsNuiV(#a;c6{aF6(X#}_JoIZx#B%}vz+rZWgl6GpY$Ayd|=MVe`6_^zF>w$;_ZZo zgSPAC{6M-5jJu?V1>}5-<;)a|>;?hnc)R z-0<{;-Gc2(I=P6;VO)~SMP?0{OOPAWrV$qVk39!};>uUooI|{Lez?@7F!mA2r;}QH z95W3i&Y%WdZ(((@mnN%#2C*j$Vp~@a)i}Uh{OxPp?m=#+oSiSW{#4C)UPq*n<&aHO zNa_bhxfjpPKNFR7xwu*1>V|3@wfyfy*~Szu8zZCsM=tCXqhV{NnqiVT? zgqzEpP#-w8amnTSk;2$E;UO!nQZl4YvT!mkH3y4rwQ-6^oQ0NJvz5ILhIc4m(Cc+# z)?RX9U_FM9<9Dx_0FrLOddJCXf`NtCjdfO6ejwU&S4yih;I7EhLMgX)r;d7&$${Y2ZTu#Vy&73 z!bne_%B5&^Sa{Zp_p^qgg=894fw4Edg@n6LwISv?APm=eX;y*#%V4P<44eYO?5i*l z?!#I7<^~m_dwmEZ`;StMir+$vrmk8esR5$U9chF{xcDs4+-3!vLAhD#if*g4RA8KpM{9VpKn-hI`#A8jQs0dks>r(-7juY5^1?lEiG%13V9>b_99t`Mbk>mX zLoKzJf!La{uB-td)wCg^X6(B!tN~LwoWiWkQ@*TQxwUl$H7VDDlBzeAe5N!_Fv%%( zEZ<^kEG4)ZE&CwO4&-jb*!mWP8bP1y)phz#Lonbss8as0)i}+~Q7EeQyRW`G!QWB> zX;6^g9^++!%uS)xVXY3z0=14cY;YW zERC^KVlrl->Q#BXdMDSqyNS10GA7DkWopu=5ZvXj(aw3a5YQ>Ul4HAv>)0iSpN!jN zTrd{)i#mMcxReK@VU!MohdgK~Y616*YoyxtlQA>}-n- zD*?`CW&(Tozzd-(P7L*9_-krL8Rl|>^cMW+IdM&bL#Sw%I~ApU?C@H(K|;>mbj&%# zEWXwd)KKXtFe`XKjmE0Zb_s=jHb9xQ(o?s>)o_i6C8U z!H&2h8D;0oXrb<-SVuT8)@RvPB$f`&v?VIu&?u-auuoP}E!w%xZ)+~ITP;0>&+IHZ z!c#U&LhWqhU@5l<%WexYm4K0EPyTQiLd7=?2G%+;0@$}7YS*hakuw#%?Djlr{t>XK z#V;f@8aa3z?{M^m0)rmX(Y>^zG2}Y;lry@ zB8OQ4S6?$s6uOO2!$jxz9_Ta0QA-CZR4*T}leuGtNM0Sq)Tsxk5-a;7a7qgntb0lB zXewYunOs3wX`NZVEN=A?8RwFAzG2vb3O9M36_p&xR16!isJG@{Oa=8X-D$gB40D8f(2{oImYr$-J|a^wT>lJf{HV{MdLJod>hspTD6L51T1PZdhpGr zsH&i_%iW<&P%KrcqH;cSAP;DkbRx-6OF2PBl1wf(tjcl;2EZc>VwHbiPVky{oYLhYlRm#06^}J!@lxwG7!v>*-NzU2s3q9M+l|20{uNx@*x+*7 zSn+EB4aaeecs7`vX}ZAS`e*J1R*(uNI%eb;JTqAb5>-=+nQ#boj%G5GsdQ*8n{Hjt z&%BcJIpy8hB!DQcxfNLkb!=-#U_(00ZwGXrEyb4Kml3)z6IaU^`EvIgi-l$x+K52a zeoeLa)!WtMu(s-&X)B?tfVKb>s=&oGxV3RY-a9@Ue*z{ll`J=T(U2-qp( z-bZtsbJ?Pqw;9jesh#5+RQT*AR9cts3p+Ztt2JR%t?Tv$=qA@bHc6QQgEB&BHO9R+ zthw~qu6w1IoQ%PnzV;-+J`R#f{JE5Ek#cG!Rg4@E8NOg>5Z-?B8c@xMW(3UPZ!!k7 zRJ z_%FItwRRmz=EbDtH(B{R8!RPw;F-%9%K)6oBQHy8fBg!59AdzV9FMW{B`#)MJ3;sz zN;K1CcKQk{ddq~{7J3kDvhZI$$zgtrd4}vB?-$gq{*-FuKs;xFl#;^F6Pn&(407{I znpxC?^cOBXAqBRaHx4t= z6=v1;=!vh6Q+YWhrj)s!)d;u>xxU9Ge5!j(S6WBcDNp~oZBe@#fJFf@{c-F+7Ho#q z!P9vyu7lL#2GfSkK4SecmvJW>G^|P7GZ^1^^uS=$EgT4bSaJUPo>N(=&_(-BRu8!F z)AuXv4fK$#@x1ym#}a`<%R^o!!^!f*8zI%CF7%}0G`Y7NE4L}9mP>MIxf9n{?x^zJ z;_d>S|2-N{)R8#ZH@E%&77eLv{6mY}?Mk1P6IX27$*I(vU56?pyf!2vBiEZ*ye6F7 zSIZ4|5s>?J`EJLAE*hg@2YJgzD06*?H|Sa(4M~R@4peQ|Qg>QhGl4}%4wH_q$O$R| zSv)ZMS-C?+R)t^ddtwwE3BW08iD+EJ}fKngokDVeACQkrkShw9lKH2 zEOVKCQ8h@8?>3gd{`_@v_Y1DKp7H-V|6lO`7rbvhA^_5P0 zxNglRw@b0cDAK&5q{hzAWNtj|A_?NgxPRC^m_Kwgrp=+F_5R34Mq5;Bi?1=;@cAL< zC@Y-}oB<$lY&`|wOMo=|`BJV4y`WJNAdZt#wUjEUs-rGstgP<7zVwys-PhMY*?FQM z$g;s^^`3K~eiR$Qrw+udk*A!t+&NNBQh#Rx`@p`9yKw!B?0aaGdXA8?LUo2S6|cBG z+3CreI1&sr`&KKv>RWK6%GR&?wJ_76Xc!jNa?FsLAcYm=^@^Jbypqan!Siz)Y}zI> zG{h3il=~fv8czz*zqy*69)rWILw(q%!|!Ml1Hj1pTtgK9pYuIQ>&u+|AB*kD)%g$3 zyk@+ujan0{!P(ZY{rLC!+E5NU-koBH8d2Vpkt&GDVo>*fy~M1>u50tI?2M;7^yhr- z7r2#WUNT%^Q-;M822}_6-<%v=5;|aAXn&qhXm4x@FLs!2|Cr#q|L2Cs+(?##-fPB{ zmkV=SW4}s?rOuglZ3Z~UfCE!ULH*NE{yD{6h%a&cFXNKtDjRE6{l46*)`E`l^&ju^ za{i}Un#?YCYOHGua72K{#P%$#J<3|qwJ-C83?kep!HypMQ$StwFpxf8&vr-(4P?35 zY!|=tNPDilI;`As;&$H>)PS10p?rsWiW**++m*#njyz~IOOR7|RNeM)j`&~BFw5mS z0zKAVaNKTrT7b#afyPFUJLw_YeFW^zWf7@64Ta>88`#zREa!Nx({lI;lEj0d)x2Q9 zR-U}|F0S5&nlJD0Y_Od@NUoXrt z3sAO-1EghVb9bUn+ZQ)L8rCeoS{I=CUtnL%#jwu|%O_1Yr)C&%5*|mOIO* zr4{L3BXl;v{q+0YIh*dELuoAnCI1|Lt=#k3_HIfx9wj3zmZ$Q>tG6AM1|m~?R*Rwv zR%xzMMd&foyAaCr;>)cRbSU(k0Am$m%Vc#f zv6O#NuSnlu)nUUgS91mes4dSq#8brIUOld6cc@FoJR(#u(YOC z(By(~b8g~kO{&BmzPr;~{SxED$JDzXzI&Y5WWQnvUaipdXh7=2RbtAlWX{GYRZI>Dvodvp*I4=%JmxM(rK2@zC=K zK9BH=2)~H%uMz&$!k4;_=v?DdT>O(gmGYMZ?i9=NO|g>1X{{z5kZDMB7tC|^4yDse zPlQMxzWalD58sXa!$%uQ__3vk?jkCAyjO8eEsnW!;7G0N(fxB`z8#n#e13=_e!|7# z_4a^nQ%1va$~;((qY)b`t8!FEQq@Qak&(isn$5~gRz%fhWH{W^-G*)#T}E=81R%FJ zjr{PPyPw}*@c&o$OcC!m?P#r3iS!KJ&xolKYoV7cW9Pq2($_PsnQd{FV)#fGG0e_C z7TW98`fzcB@i2G4GJhIKS9@4}uD>mq`U>Khh6Kp^)WIU~wW}2HyhY6_R<_V_)xkMn z;+A6RP6T%ux!uh(l8ivd45+XsvhU!!!)YpE8}L}*drrb1jW9oo*W7x_q`f*q`` z+1|0k(knR?Jaos+brt7Qk#6vP&+@-Omb{?OqZ2fdSlzmd4Sg-yfWx zVpel=e}iyd-xL%px`g9dc*i|lU>J0Kdc2FItiDbIsw)cSS5658?ETtHU;+=^o(x&c z6XQr!FXb7!&_w;XZd&B9aqoQXO0 zi{#J!>?jS6t`ZMi6o9I8s`qQ{l2tl#o1vu~Z`mRVUdv44;Wdj2>oOM`-NY_2)Xu#i zVUJ~)q|^%NHmPTeYgQL-g6dpREi7zNG>)>deTYh8$6ll`oIbD_Tj*li8#1M33=2-V z2&bT)b#<7Z-uN_`nZ0;wUs{5&ZG?2K}%kM zhZTJe{S@`REUXl}ikUtwRuvldO^MD9PzTsbLwfQ<_6(dG zV(qvu+7aE=$TA}!m@8^>+LKa6kP?&FF`Mj*kAf! zL9dz5+=SV`h%>#!VJb_y+r@WQgT1;d6SQsi8|&LiF7IfOJb|cpsyXiKptH1@y$qG| z1IKu{aw*<1>Q#NakB4syZci{;c{f@z1E4Iw&NGzCI{>QWc!ex4Xyri7p2K|YRdQ3r zD8@`h0=KLgCj8U8OZYcO+%l?Rpt}!TzBKPA!tuzf)gi#YKtl z7a@gR(C-9sCyPx1O>{NueW2C^ZT`}I@%Z&DxP2q5b<0pajG5Gr(&l=0uZ!E2?xk2 zc_DH#4IN_GA6!aCyYlCDC_FM{0p&a;zEqVN+NiJ$0lL2)BB@1-3#lta_G1;7Eu2Q2 zUHKGrHM{2uF$YTwbsiA z#0Xf~=)pN7qlh=cSAPOz7EYjVZ0aY7l^jiC}az;^7Iu7RSz<{t~RY$(0cJS zf#gbQOU!v4>m+;`aUiU>dC*o~l+VlZDb-sLEX(JWjxN<3Y~5bqy;(|*`_DqtJ3z}c z-wO@)QqA%x)5!E7vSae=YBY^^YUkxney`dwY--7%h}?e4~%zd7pfC;t5Hktl$)m+ z@YBw7}JHyJ9Dwto(&huau6qb#z)SkC_O4xurZrfm-E>) z+vR-Cd$8rq+*j;MQv(n%d7`d&9yY1ZTs^}QqrqG_Lu#cj_#gw0*Ie~qE&lD* z5`#o4VtMK3HMwEvaH~0XnsxJxwAdO73#-SejuM%R z?8~*r{tY7bZAVTLk20f0J(%ih}5LE36WU?zN6f@!M;@{#lRp5+cZ_{bc1%O+ER{Uk5~?wg@j_f zTy8I^WY*#GfYs6mPQdkQANvL)Qbw9?rQSg606e8KY0IDDCuCJ%v@aY5y_tTT+&nDa zVNRnT%u~1oA13VrRv~a^M=P&k=GTgQD2PA22q4&Oy!x2h5WVae?A3T?lg zdd=buZhxjC$!7_vq!9{}b{Q(Uns~%_jnupUT3;khycQkEasqx*xK!5hIJ-Vuf5u?S%;QNQKvB51UsueYC_=r?xa(w8~5Qs zD%x$a+VTTk#1k*ZyG4XljVm?^*3kALh`P-&hONi*$tzEHR2e%0Ce09yE$6^yYRi^SOPhFL&Ni(h1Ug?$ zW}IFoFNDX=5h~y?h9}>}sDQ&5sr54d0GLk`iG*h*`V?XH&h?KeTUf00!vlnYG`x-C zomJiOG}!qQWwx(3RSLV$1hyj%R-*Swmrn;spE^K_Z!R3*3{#uKon4euh`HHk38e4v z0g+p-5?6+#TO7$Ba29<(q0b|pYX=49!wSWk{w8(FEqg^MWWT19miop;kG+i#(B~=T{EzI@~?GK!>%D})2%m0 z-K+u4$C87$8Mi0U>R^cRa{IWsHvDT(N0REG zu-820&46ZJA(aEtb7`TSVQ#Exk}WiF@p;5j*deWq8rZq29d@SK!qEGK|zA zORQ=T8OG|68v3jJm7wyUah_B+>h{}A8~@|h~29j#x+?-)Ap$ZlN@9)~I5wDtoM zIkSlDIwLJwED!-`7nK8aVKiXiS=tvrXSmLX1jc$~UTTz3mc@o9$#dJ0B+Mg1UzmxY zgfi;^RCK+_qfJgyFZB6L9zbT>%NwSQTiwAoxj$i{fxDN5(ANmVhAAjp^V{BlxcW75 z*KA@ykr`x1oLWNN?ob|9@W+B;-i0r35FBmqq=ZdVYU&E(QaTbROSN9IjKZP#loleR zR0tRDn6~^twgZXEJJ0bg*MGUQEYmtuCYp5b!_|GI`VOJ0ATBqmrnquqh00ml$A4;M z?%-Q3FBcX%*C?fOb_#!=acsDPN*7xrpu0b*;_UTXTO@q*34F(bMe3(ap!T8jV}9;d zb(RdLi;Rzp`G<`Sl>B(zHR*VuB5g8WHiULXA&N7ZvQA^&^49F%I7%BuqIX7<6H1`3Ql~Avl>oGNd9nNf8 z9{Z?(Xa5&7)>C?&Z3MQTYacsPZm%zxit@{jGyo2slR_m;9QVf7v9Mgi)~$9K-Sx0Nef*d(fKkz37Z@kxH2?rX}h zX8sk#%%2p0ve7Cq}y-WHNlvbL(gYlG~w24Q&2MO z${RJ9O`B7LLT#_<7}xV9UFjXh2M>1>9J_zV9|Qgr?&vawTDxa*8CSD-I%ka?=Z*5s z@?AHZP|l(3X1(~|3;YelHyhhaxk~bvdvYA898&o6r9;(6>&L!Ad(Jt4{6YQd2gNV^ z=hsO{eU&+Clk;wi`f7P~g;s@f*dX%yZi#g|C;2ke)BPo0>+zd@ewSSiS|>4npIqX5 z%d{RiPvPQ*Sz_W(pT!%dfM>d2W8}k3V<6$)@K`t8&Mvquelx?}yAJqS=xE!XOr0he zhioP|z`WDFS^SeSMx)?ZX*y9v-4IXBnHN|Rk_=51Ri(+xqig^6q*Qix`Ip*8ZNf3Zs{DF%GO+C zXZtvT?$*ub=O{7LVBz-=#^6flwi|2&^{-&#-u=fk<(C7WMA!)%!$q7WX=M#P&DvCZ9o4Qigs$f+qa;B>smfQvJ=dYOU zHaeBzpoSflOp=*;!CLJ)&bP6?)_xsV%OZZb?Cqf{@{b$#BZ*9a3tsX2mH>#0iCZ`J zXD|gG!)I{%IJCDX$@oLZX+V%zW3G$m#>IDLd_d?ov)B`~o3mtvx98d2;x%k^G+kmdo5HT4j!uGn%bopXW*C9dd_u%)9p=Jz+O&HUQsuT8!5gUWk+Q_%da3hnAJdVXo_87kGlmQG{GIOUS3*z<4hAS+FY}iao zT+DDn4xYbq0hr0N&+FTTZ_E9N-7!}jv|Dw>!MkVjnu0U{+@ayWEA<@8Q|Nel73-ln zXOD`dP>h%gGCOT?rNAcDmlfwS<`vB+`}?^9BX4oQnTecj#j>#T4PnYq9-KY37Ktq9 z!+Vx0W_wWp_niF~!KD9nGXRi&1^_hWWorpaymd{;O3%z({KZKc8h|Q9&p-S2)2<0y zEH)m|K@_EgM9ZgXimIphQwrz$d%HtD*KMQfIx2G9s-5xs%f6*6=uv zhL7O^Z*N?A!dnrupC%pybAb?pw?y=?qCI)&ZU8Ww^qlx3iin+ioK41hUmsp?YAO%GPNP`1095K7aWXAD;sCEf-6c@RdtA6nX=3cln zR27_|B8j`vK__$z$uWn-88wV`#KDP7hF@0>hpji4{3R9SfNnYxHz#GW#wZ%tnNAxT$COdm+{$kaoFy=_iehykW*6Z49)ygW7~{pwVNS1=8!m}a+Y!|5vOEa zg6f1Fk{On+Ye{=BGw;YUJSXb(BD1w&C=#?xOxN1t{0P^cr!xCAN}VK|8d*_4ii@8Br=!8m%_IfJ}z%i#`$z274+rJO92ot+g)-#x7}gm zkB29z-`H>NbWy(o#3vTJ?h~^K>!w4_wj8frbJ0-Kvn74Y3l5K?ouk0I^e1kEttS%n2BR}=#AyhS9 zpmwHzynAmq;3RWa8M(re6OEWCY%HnANCQ}pvYs15qmM;~^SbTEhAj_t;P@f8desV5 zg_cYvH-!aa*{&*itrMs8+=mOo8;nid&O%?ut2e2BCLYFwRV!**=oQupy7%awfX&+D?8RqWxUakCYG4ox^`GAXL)OOWiGm>J6^;=9-4tulNqZ%``nji-2t6Qh>66iE%iiow2zBiI`j3qm|Pq zi1(xF#JYja?LMLcs-&wZ$o9Wd^w*s4Z5QD%;{iIJ}H zQbne35Z5`4`;iS7#mODk$K`6DsaN55y8$MBw_xJ7dMEO{sf>7F^*Ti zYvy%_@Fk;BbhDe$HRyxwHCt(>cYLy;%}T|8gZ0F-=YK<7A6`8F%Ht6B%cBtX%VQAs zihClA#(5^+?hBP2m)XQ1ha6GT!*4>;Taj~gmaa${_dagGg3Yk11((hUr~mS!c8S~Gi)0V3eZ2e#GEm|LPue)m zXBv$^od5Q>$?F~4g~f-nw%4mYF z*uU<%HnW?UmvKfDvzKP-Vs$GA`53=h5ha^`%R`NaVA&}w*h89N>(@0BOAcYe;fZK( zto?QvwA`(65fKh*{Kr+CWYgAlItSpe=kg8yiWw$x?^Zq^wzod`2s0t%(*_@u(1*&& zKQ|T0KQ~VC&rKnK@pBboqCd|GF>ppwbT%J9c+xsP{&5-~mP?BlfQ1c-mXjUkEB3Hg zA4Lv1hKx0<4a4Imp2%=;?sUy zj&xgyP7E1mJuB{xnvn=+#@4GWqZ%xY=r!E$?TU$C=b4yux%Z17_M23%Ms+-=x=v@0 z8dxUE6`N%XSbHM`t~@OqyXDy!#b-&K))CtWx8?QiBX&_6#%X`P(evqUOQca|j3@2c zEj~;*ShB_N^prIv&$}HVzvrN3P-W`O48Wr--5-~!9>raln7fk6hFT+a3;1UqrrR-3 zkvamlc9tGxv)>3;xerVQaW<5WF$V1~{(a%A2GOrgK6hVZ*ueh_1B|vQ$G&37Iol-K zHj6zOLmwB*Z1$il$E2*K)5LGtxgmkw5o6H4)|w7OBnz8Kl6|E_{RF{;$dF|p$5w`D zCCmzgF2J_c*oB9g*^1gpZvv2%p7e-SiCliy5mJhs1nr{&URdvsJ3>Ru$in_;JpnD6 zn{ZF8Q4*3L##1{oUm^@`Lyi)|R#LV<(FO~PCOk->!Ky*#;mpO<1UW#e$;u|)nP4;h`4Hy)lyIQOwWl<_%6 zTNy(<6=*r|9-~!{EvELw|1;_q|1UAgL~UGO16f{jEya^5snR@{;}1$&l`9kRq9%j8 z95u+-vuz{O*k>z8ixbAdGki~P@R@=2tA?_r#w;Iv>VfF89aFUJp(JSlB9sOno8^|5 z?>G?Ks)yPb%O9N}pJ=3}ywNeGz3`i5=>VvLdCvZxe30nTa z6S$-9u{wo)GQtP}Wlq`VunFf`C#yo$rS~-LA?6P&Us7W~BfqW8Of1jLgV1X<+P2yr zqLNrx6WPX>@oqlR;&OeJ@+MiT`eq>mE*cvo3p8@tK~?(0`rO8|& z1L8R>AhJ;jbAhMGE^P(Q+XT75cQSiE#4_>Gtg}zHCd~d=cP5IraE^RiybK2quA$r^ zn=RxT$b+q%&e_lHZq_LOMAgH{b~d+eduNS_H4BX;7mWDr@HJTqvl=|}Q&e6QV>8Xp zV1(p6Fc05HE&GQ&Qv}DnUVUHUKi;%FD%~y|%C8@G*}|`fwRQ5)*GI>}Zt`F%WPA8( zt(m)|EbM71S<6Y>mX|pgjg!Mi$FWB@UfKlcN*B)&Tr9ZRe@qqWwP`j5`H;kougsl` z8RFMhjM!H)uAvC~<~xa_<5$@HUSs#IjV#XAv@Sf8$!u&mjXrXKbvm8q`PmgFgY8^!e7a+6en*vb%^WVmlCFrClbJI;yItHpQx z62yK(1*=-(-zr@m zqc=2;e-THq{>wDeq65mgP`oN9)7Kdzs$bCarm7N?zXr3v1~0)rtiG*KAz3cUcT{r% zc$3)-dF}QtYB%qs+?}{f)3+e(7U#G;wugYt1B`&PIY)>0Tw_>WSC}KzXrqWOicLI3 zeQkD8(xchs7UjWxex&pVX^*E{L3`E*>`lM3qh`?~ub*okgL!0a@ViW1-)h_BpA&o# zJw|OZj;inb*2jv;e)MFk%ZT1_RT{3MV3(~u2sTs5u3%|im19wQghm|1VFVN9K79PJ zGaVR%&D)rhBd(92T7Kg)!t+R#_IkxbRW>B-;z^2@VKo<1}j z^7znf@>%BE?>NxAnY=>U-stTuIy`5IoHMgHoKJpVEWX*FId}#54SlE{iMJtFA9qG#ct7kI0-RxG2 zFk6Xaq{QhS=of|?!{mT4{u-D~!QWBaXwKfRbP z5^aQq=isNpmQarScBaRZ+&(-rsDVa_^>#d?a7+{Nh zMqEO|h1*>2v;>KOOx;`aDSZ6O&u*ljK>{~oLN>RpODU<(whUhdOVB`m#YZJmv0SEeyG zmLaw22EL0t3cId4ULJxQdIneBqr!IV=^n*hK}MLBo0`>Z-}C%569DINLk13KKQ%yk z?st*&d=Fm!G~3F`#T%(dNzO0^J6&D5A4$RBLq0o}r0!Uo{Xt#Q9b4$q3~H$z#~_*1 zIkD^;z*;8C=EA4^8sYqeoZCwBoW})yLUn%FY|_WgWWnu2>d0O#xBZyNV)ls|9@dq! z8Nzk4DxJc-PSZ17XLgv;v&Du*{C_j@*Q;UO1zG%C!Ku&(r^oF*KGfW6oq=7qU@mIh zdnG8AZpj6iwl;bI}cq0^f+GrkHJrNP3E z1NBe(^Vuw98tkMg(@15Pimod;7oo3}<*q4irc+VdLJhN?WBOW=vDrM>$kf0p8&wN? zaGWjs+bt9HKlmHfeMsgK&ifGpceSVb(%}x7YYu{Y*pYlhdR+5*$IxbsY&F;u4^q=m zD53*x+b8#UsBR#6+noml7T)2s-X;J1qe&RWT>rop1vT88b*wIWm`Y?YXO@k=e5NLT z2Nyjv9IjXnF3jiZ;uUwDP~Y&xfX$$Lk&1b*P3U4*3t9*2^EZ~ay-9SuaI?63z|o!8 zh9TKqW~E32pvBIWxkNDS!pc3@oMhpi8Ps9nIiG)r)`dsE(~E{xENLka8*3Q7z>pGB zMb}{xc9pQy_>A+$|=eCltj$-+0iwc!hYoe1?RCs!HZ~lSDxGKU-BmzSQM=d;p84%4!)E9eb zI>W})Tk$P$yphS`f=z8R_#1@7OsX#H#41&F^gfLQuDjmIL8UV9x0Ayl#IBCB^N9PN z%N+_StUg31JIAt2AfQSL%Q2qZZNod|FU~5D(9@Dw?!SaJAmlJj--N**+hy}bF@*;9 z)(0RfL5^#*md6@o#*J2r8hVxr&;zT2WE4Z!!|lDIn1jTo`f`m`wUuhB)KZ;aEi@IT zmIG|R5%i|hSpv2ZSd7Tkh3d~+ldXUP5#7GsWRuu4=^wb+0^UzaeU3FEZ>wust^{#n zEW1x?+;tFCpH}9)zooTt`_tba{`Z zhFu<-_$j)l+UByxc-uQw_3e4603#WwJV9_AW)HPu8}@F2p33+!li76b%;$`U;o)s8 zPj$3+xJN6U2qQ<;R9DmEG&8*Z??fHqtu->9{%cUGz^}$N=yJT=73!E7*v8FZ#UAN8 z!D0lH`BXh1b-0Zdxy&FHO8$K7pcTjaWBKg+q80B0_iJTlmjqVb=kT!-IuIpvh_yd; zXMOB;1qv7ruR~^TUua9%tFQW^8CM0`M^B?&xi4yb9LP2Hok+7Q-cN53XhG zwqE%x-sHoxw);2y_bhnl`6PB*C#zVOGEH%h!6q4p=+`q;hs|QFbpl0;zcv1 zprcw=nWP2p$t;H4dUD>jKW3!l%i@DA&ZS4xA1wG*26dIBH*L?vn+lCvsyiG=;}nvkew-??&cc&{$+eX;vv*iJ5O)4t zS;o=h?X->WPaEkf|IROEs%GgN9xqe46g}B8hpdv4C3H z7x_uNBqA5DDsqU#7^;a9HPVv@xFOAz)e~n#L+&-%(>hdQxxY?!Phw4^pQ>~Di zpFbC{8krf^sFM}Ud?J`|V0t(CFjAc+JV({}fW_Y3rM~#!;Fs5PVGfRitxb}OV9x-8`cgYNSw&tXe{k!W5 z&u~%9ExSvv)wjzFJ$J&19lq7<$#nT~oVi+ny+4P&tkUsnh<%Auo-#?Gk=f-i%3usX z@ThgB^|MsUDW0wZ;(5pECm&fVtaCI!ej)4rVXJ{FFHE|&ESRkdFd6!Rhht5S?%t4{ zLy2Y=-%zE%>xvk!^V<_n@?pEg{C-Il`GR3IVvAH1xt>u)Qm2~RZx>T;YHSzJ{{5Gq z{P&}^0oXh?4XI`Q@(UL`dM*Zkz6btd4;=rMJ)cvKQ!Pi_m)w`ufMz7!0nJDr1~i@@ zeB~@&JnQPF2jYq(dB;{6dUTuneOS+Tg|$lG8@Is@;^xwntVKgr35*f_OPsv={)E$@ z{>GB~cWd-N;q;fiM0;x|r+Q0VVV$i>hLjPk7`s|lc zXg$zrNZ^0d3I7c5P23q{l-4XwqD3xuk)SloV};#AIg;oe%8?@Pp>z>$@@XPC(vk)r$%Da1QZ)EJ z{@zK3>3TOA!o8ym;oemS@O?F_JQ;3JISe;amQs69$Vwq<8hBq(y+3=_|N89bU!TbW zNDC=9QyHuO3qJN8@$Ea}*|&=%HYkg0bGIWP z7IWKgVJ62?Ty=!?(`=##U!1D?q?2Rli9h1`awjtkI~=%b`C&5h4TF7*IIT6gak-Xt zNx-m&P((wiiqs^^g&9+grc9wv9t?DN~?`|h@PK_&W zG2p~k7PD@gT8qa7)5s+r1}+_CI>rv<7^_g1=%opJ1rD=1{C>;2&h|;Tqrl!SbGOh) z5;a`gVaz8!aIPz@8HQVOqFe_>5l%1O;fK{VW+78k3dmNj2Ok<3e;C|ZI@!Ly+i(Ej zde@Vs z%k2bpVrM$Zs9_YNc9ki9GOiDwu6s|j9xq1_K?fSi{TM|j9yT~(FzW+XQsrCd6Y}r1 zi`A_+*xqN~)V2xP}>nmEKW^bCw(mD{;sfl!D;G zEyoM9Osyf$i6Wj|}BvVa^}67g$N^07GRKz5*qx z(+3`Gd^gh>lb(M`lpVOC#=Se%OUV@Ei zQl9Qm=yrW=8W!a}eG9%YGc|XKz~E_R>tP3YRN$D#AMG+OcID07fhUWTRkSQ^IL;65?a^+CHtmDVe%GVQU#YV9!XB!*yQGArI7loGwRG{jTn4qs~ zB83?s=N4(|lK|ZowHWs$!on=D#MTs%u_>K%Tyfn76o2vPxKNjgSaA1Th0a=LG(B<% z_F~&h!||z^(~dMET%P1`eEend;o~#@Kj;4o zxfWseN6%$X)A}7c>sfd;zr9$?gtEM_USJ<}Sq3d*fwYYDG?=kDK~c%dYsH}%6W3=9 zp=C~$!fhmNWw?Q}Uj0Lt%9xSfaMulO=8w@_a_UA0{wQNljIX#{$eYooDr`#RR}0rG zGo0#@d!Y3!lXe=#VcZ1uDwZ(kKi2lKvoD=gfx|MeVa$2vTsLTpfnsQhW-$_GH#wVe zKz?VKE;e3$Hw(*?jbK>cY;L&PFuhrzrhT(uz_W1m=_qoF%Z~L=InxMgeTD1h@)^|2 zEloRlX+LrvK5>6#Q=e`#IZRHTcQI_g=ad4xAOeXMNj37 z#i}QEHfX=x&fX|k-O(9Zo|z=tCjBnB|a62KPl_>ce>PUhA@`MQq-<;3S(xt5m9L>ld06#QS2Pt(5Vd&2ihr zxP7<0)kY~(|-c|FH*Kerbcx+Timx_`>IFSv9bv<93uLLq?huye7EZ zW%2T$#BdYqDLi+(%NC(-*K3}COdo=1x5uH<4_5w$sKW`by{0T={9?8!3dCq)#4C1v zJ{yrM9`lK7N*ze?7J)z0DT;E{07PL0ElF3wg4N`nE5+I><8o_de1;5Y7&8V`z_RJe zScHrmiz8q|eYlbLCI z;bI}&|7o1IZ{ApVj z=}IhyGkh0T^K3@h*j6Zyc|OXFlwIl4@95{g-h*^_66%e?h1#t}*&c>xYzJH8@C|ce5+*V1)47!o}6*x_UNi=dIMZK4*246YXmgmrqPeG3FrqvVy0rc1brd9k)0-M2_9&^AjiOeu5kV!I^cS~F*|MZO_smov&AJw*%#ORA>y)@M) z2pXvL$vbm=#%2;GbqL&LU?yxy5*G#zcB9IieQ8g@eS77v4rlzk5;N0OutG<2g@8c8 zgvQL+E02arc}~%)aDz{g6TXY@X{KW4G)q<{sJ1q2;^jrLE}-Y86wbq{GpluyNtH*# zlyBhPR%t&@ji*nq{~RKk+J>b?Y8ZV2Ww~aXe5;5Q5JL4=hgF;$j*6(mUQxl-D&JC5 z5^BzrbiqXBB_2(JIa_rWm2>9<)67sxP-GNsCnZc%LD((MB1r;o92mUtbztU-s~-;fYu39gNng2oSwOnTW zmx8Zl%kW7=`*aihlgG`$eRPqBL^sGsLPTD2u;TaBEZNFx6=~1>R|%Q~0!5B({$oA7 zvnd-Fw5b2=D&O^=0jd=H$a_#2IB<4|+}FIH1pW!53XP1~b{O*;?TSrE1EACl=o{aZ z*_?~~QL#%z6^Kq@44n(?O+-mQ}^wfkZ>;Sd6{@{XSxf~jbn_T4u*B~J{HYR zjmMCR$%*9k>U^E!*{XB^G)g<=RX+T@S6`eMUWfaR3(lVf(yz#|WvEVxg=ry)#;ptD zBt#?`APStdVy1vKMFkhHF}Uc_qKOtArnv-m6}KXcB2pfP8CQmHXx$m zJr^z^#7n$NxdEgUHS736mQwPNaJ)6=Ex@nBU6WUPhSFkFCb4Wea8w7toZ0D4CJt$W zY8)Y6`(#P1_u+_#F20o{=m#*%5tf!VFrA@C;Y)E%wiLlWfV_3d0u*1sFh}SV;Q@qd zhzlSjl8B~;T0l@}xgdn{qu(rt0ncIZcoZIpC1hh@s6UJeCFG;D%gJA7Y$}M9+ zR-wAgb@}SDyxn);Th+InXt>sB@Yz+9%_q{VQ+wm|5UsT@Y&(DbQ%1M!)v)RxPvP$xi+fehha|YvmrMssm@$fH2o5OX~!f0 z{3Ags_6b*k>>R&7(yHXrhYdDT?jWWL!u!l-Swq6M3RPq_CXX;TcpRCP^1uVjylirk z-s*O+r!l~1u=aZ+DP2o;KvgB)jeuGT@oICIc4#9hBvc#Wbz>$GWmIPF%K42lQU$5g zDwhR+Ie-MeR1}{eI!Z+>_YqE zRB+Y!Rbz}^Lc*!P$xcpHl)eRD;|b16|N2!e3eiks(^*EtbLtR3FA>6y8+jw zivEdk#o3y)#H=vlfj^1aCaI@IIu5XL+EC@I;*=wf!CXWva{W0T*2-ni2q`nN30tJbeN^sYD!19n9D&_nD-EWxBkYb}o56ooro+oIZ)e<-9EN zVCrCtt&(c#Dg-gkYH^3K2`9WYw_$7rXoKbI68~;}>0CzWx;Ayr zBXo6ZrtuDKqf1vspFmPY4n;4)q=0m6oqmEz?daG#)iN@skv5jjAHh)qo@I*xKzz>4aHso z7LNuX3O5srCzPxrF^TgP_#RY=Wrl)0Rh;X;jyHmgJZExKS9qK|nH@zd23XYJ*FP6H zHVbp;5)w`ZDi6CduhwDVxi31;Jm0z3?XEn zvCHhT7{OyNf*PwtW-#XXAY#usU)H;nX4t3$Rr@tV0HRr1^0MyfdwQ;eN2(G|xrkAZ zrLWcfYP?Soy((~A)~WU`OTVpiO?QgV9iMnOw97jJr*FaSN{+2h{mWoDn0JsqXcfz@ zjCW~>{4GwYDByCJjoU(y#up6I)ggfC6X7!emuara@?)2J!1%o@X#HbXZZSB!D5w2AleM9*Jbo#k&nA(_$W&OT zw-+C(-hq<@UBKs&3l*w_c2&rCObN$Y4;;Qz@9grSg&yz=Z2B2pIuxS@Ao?raX_h)8 zANl?Xq#Aby@jINIrTdjSrM)+}H(H0VhYG#I#jgq~C)@Q8Kl2hU2ZVX}9#?+f&350s z8U~Q7+vVni)tP^ZiM~E&*hdM?``j!?b)dr7W+IW9ZaUvY>L%c9kam4xNPM1Ma{ z?9tJBG)ITMT$?17ubfMrsc)AT4|YxWTJoC2p~39H*rlk{vy=G^!xk3e5wQg-j?|t9 zS64h_ymJ+*C8CAdudI@*13rb(|wD_99l zwK7v>9;FW;xxX|O2E{mn7{(jh%d6RiIMqSt*Vd!6YGKf657JaEbORW42cs1W@i>AQ z)>2hfLED3%Dvbob1ZqsnBhyr1s9C;5Wy7ZgTvo@W5olW-1Bn`!nO-)=+P}26T{o^~ z#k2r6p%@KR1=}HZiKYYpkb?w2%DaZD3Mvv;9L$?4gQHwQ_$9;vP?S}naqSf5AOm1W z`8M6VQ_>Z2hKoM#Ftv582%`waVAauG1r-#pPWLx9b?73I)!Uzf`pRlpbjX~lHI?!2 zK-02A^{y6|=PP){69G*1P(h((N9uR=k_nQS4S`U`(h9M$U3(n_`>JnT*bHFMO-tYQ zriE?*Yw1$Pm9vU8gQ1Q?ytY{(AB972W4oF!Gl6&rgwiru8XB?=GN7sIcR*d?>Rfmu z*I97LZHROgt?E<5uG81;ST7hA^iFo$V3m3&gQ>o`^pnm{`Sf+z0yhtSOB+ylny4TL zKbO0ct8}^tN%e`z4h}Q66}7FQ)bj9oq3JWclF;P{C9(2QD~AqCq8Joyy*e-pWEHgH1&1t_6+TIk zgK*n^`)K-~^L5Igjq|fH#7^=whL9Vm`yAgwseq_3z_kT*SSnypv;}lvE?^b(_A{;NhX)jZ=im0mV{)p8wl}d@&_#28aUPW*WS0 zSQSwZGrDp;q0n_Oqr-a6cd1$n>*<`YN30_&} z;D^SR{Ps0r4aM+nJ6L?5-EHK>XcQGcmkAN)cXd@kL@0Qz8d){1q!gBSF9YKR(p= zbSavgoM#u=$urg@I2y$%Pm>42BHY>ypwZA@d{*3jJv1W$AUT)Jrw-IH zVtgq}CN+ziQh6IfhY-f{dzoSBVASWoH`dDvoQxzCpYD%Zj$Xbm$_miKaa{$X2kw$6 zUy^R;Zo!QPT>H`!qb0gX1qlWUZp1=`u#bvQ8?%aKgg%j_bsDqw1)X|w@ST}}6($uV z@#G~Z^C|O)A8?ab%9W-FcItQ^)O)<8(!XYDw7Rgdz=z@VMpLFug=Q>rie*eaLhudT zq;X0v1iFe@q58b=p!Q{EeQ?9xr(r;O*>@5`A+Y zqzx?mf7P(vTa0XFK##OTeU`;6k`qC-S@iB;~Bs2h_*)0biBHW8q z%sIyfNRYY+)ll7c8$~FF_dC6z#ybJ{RZ|i@tfCtsOCVA0ZWoJ97CG^1A(^JvPi4{` zp%{!PJ=khu2o`k|b-KQ#Z~2ayT69aQeK^Zk8^IU#Jg2OQEkmg~^W=mkzr_W)uDG~+o=U%a%VoTh=$&g8+ zT8EcSD@6Is8E(rwLIA^5-j=pP#OEHS18h?41fr_}o4%y?N$B*e?uhYbnh|0;u$kFc zB71N?tL4eo_wFbX!VrkB=M&UnsQzM7v{|gsTStB7{q?!F?#{5I%Gr)?xvx|ioobrZk`yl(oiFGMZOimoF_OJA=IEr@Pks?jvs^o$S*V|zMy9KM^UiK;YH5fEgFb_> zMH(Ptx@c#Dk+Hpi%J{ucxMCr3YO7=RFE>lKEp0VecVADmE{t_nt=nQx=vNiBB9j#3 z^gDAffUPJ~OoPPDO2tTUz2lt~V)IGEq-9V=E=%JqBbv>>Ck0A|DqpisfuE6z(jygN z&a&P5o_$m-UL{1tvas+>cs_yJ$5a-3ZpOj`?PkuB(@ggY6MfN!8H#sL{nCe9^>W#OP4A`PPS z1f=r%=jN83wk`14$uIT<^KLv7xWV`2VS}f(xoo-IY55?{E`gV%#)xTs`NTHsyw%=D z8YMZxz$Z3*7B|b*wuW|`_C^BHa#zd}W zB2vr4%Nm;L$I*h)Z8##Dozr`HswR%1t%HnX8kT=TqFj!X3{oDZSejx;oHH>>)%VJY;bfw#f{SSVAE z?6#hJ)(M;+{?Kl5xOyt^{59N7G^i;jk;7fMO^NmNXERe;88X6h;BQVKy0 zo$Ykej0M9!J>H=jD5mh5pQ;Yf%QU;ZTK?Mg8!a`yHru%#*!ma+VYkXr+93y4)^A#f z+7;!Wx4L~~am}aWCPbaZJNfvTvvc>tgg^vdJJFHV{n@8Uc%M0VDO{?Qa0f@;wl)ft0ju*Hw01 zfA$>(92BnPAeW(lJfmsi)D&iYnQ=`z603CVa(-i@K1m(Bn2vnBdpkcnwO`{4;VVZ2A_QH?}O-f~Rl6|ES>UTku##eyY$NsG|Bjq)MQQ z>dTNSfkee)fQ#9-?&`|x5Zctu6X@~SRenD8X{7?z@Fuk<36$O#L6zd?FE#5bLW>wn za}UK{9$k(4AX#d-?1y^7oOoA%Cbx9D1D~_J^3vGb0y7Ka-ABn-OQ6RpiaK-4QKUm? z<}@BDqsL<_Qnc4+YZfa^3&|{o#2F>-ZLEse8@%QmkQ{wToKY3$h$Z6ARzyS{C^Hx# z&$xdrUfSUnc`5NSPRacAHH)zEnNzCJ8CKB={a9khXQr&s8K&4lC-fgBc6?^a3Y}q! z9dtr>D>Gp`P>wl5o^d1b3VBF#uCYSgV=}+}ku%ADE{fbNCe2Dw^h2mrAUbOFxS{-|ta&pghDlyZ19wfar>=LU zVqeBXS|jR#tM-ILE#e?GRZ_2eJmFeM7NH?=Mh#<$_}))IgpAJ|p+aX^MOP;k3V}L_ z0Ogn?-uPt8+1=FmyX?1?AmoK*{?X<$>!p6fI-__+a-1_8$L|Z+~?lF$KS{z`b`hFbhLaD^ZUYm9LJ~Sb9IIF zojFwZoK|7s<*nK7S9%A}A;aCRIWDczl@2vVf^k$NysQ$b5*Uw@&g@gcl=6JZ{eisl zFQGWfcgs#*l;dgxmW+85boZ54uwTw@FLmc|kO9gX2~MIu&h;}|c9U)(UVc~aR30~W=vJNDwWEGmNZJ%4Ej(|norQ@^%;4pP3Y?Jr8PYESkTuDa{ z6F;{Asi|7XXj#UKGU+mmo5QB2Qu0-mbTKGS6Xp3N#Yq`#GncTLBah35M(`!0T9`m{ z9dL7trWE_zojuK*hiU6t720U-d=uXi8Af%n+#PMP565~phOs!Taa&(&p7x|1;1;>lj}y!B(0aZHt)%XDnyIjNGZ(irDb#;NBTpmYqlT`%W+ z;-seBwY1ShaLG~*DBHMqDdS%bU>wQZgz@!ualW492pRu!xy8*xnlVVanm-7c9*AEh zV1=8PV-?SWkHNIGpS2hz{oAtQ3k$_fkeF(ZbmCvr+%NAI_BwPV`1R$7>G{dKuUP;F zG5*KK+X~z-4NR_v_(3UB);zN*6x_J6ID2glWY*iL23tQBVYfc5RaOCAt2G2V$O^zJ zC@gAxHytk&9VqiCj@@%g1(c~i&X^nW>}RPJP^LP!dmlbt^w%ambC>RmDx z43>DmyC;0t)g9;gzw_v~N)n&So1EVL7MB4G*8md5p}UP#T;U^_uvU>)QImJLP$howy-ESK=}oroODhq%bZk<>+Kv zx8)vKl8?cyGJ<}JMgwc`G@kj&GesI86?4v2Np2o;8-2(6?)CypbdNQPP>kEs}%YW#gT#k;(jacVkJicv&eiekX8G*Tf7RJ5EOzBQHV%QX%t z$}BSm4|;l@99;|}3=t9IoYTjhuKnOEz*Y##4=D`bL4Ou!hz)TI6y?S2;qDrtSd{zz z9jKu?KRF&D%uvy4uEJ}FnE@$}by#@UEExX0IaD1*CjCO5_ zE9|ooxahfjlPOEyhtQr@YdB`=*uzy!GNO-haBz6&qg6$1Kv7;gzk@BlXBVNbM-SA&hjZ=&tgv?Iw?@XT*t#7xj6SaGN$MpX`zY!I2{ z-l&@Isdb~SdKV06J zEnWUJI`dBP+FU!x@>!b#^R^+IFs=q&Pu3UzgV|}?Y--@T)SVU?GAuUuDU&6HeZ;&V z)j%$ctr~}s6PKa2T8&BHdoF7X(J7O$3RgDw#9d&>6&VQlrrOoSU$9T?`X$JXIJc^V}NptnaS z4lS+;?ah!g61t&(I!hiY(?m@6hI^9>PFXXiDg-xjjh0d z4HsEs3n^gjdZ}9&Pg2&DBvT$nvqL7mubM~xdK@KqWYqCCE+|0f&*ALr? z&DNp6Bk|uJZf|p@OXI_0FH(tJ$qW%;4>PMOc)asg5!KEgePhz#iZI1s}QU8SaBl|wq-~X!wB2HDL zfTWYR2>waZsfho7ve3o-la#F0J`V2h*`n}&74=VuQ?C9W(i>+{_v74e{jGHGv$;p^ z?N?PNDqW@O7%^(aK$5N&s%NEd^()xV4VGm&ZdAy^Kze6E zdD=)zU9xCF-%7`4A+JaS7t4{n{w9S0RcW5CZy&F5wN;wsh#`vCA++7HU&{x8;_YFR zL(rb1N4Y&M`IPQ%aBYv}*8SpiD|2?-!%PZ;7^^Kl`Rgv~5`Jx0BIbUQ;8qzi)g7;N z0syzS>mPFRD;HNEHn?^2&)5HanBCfE+-$2WP1fys{RRq>#HL+!8 z`oNKy`PH*>%V!ia>#pFw3I#Nqpykzrdny3dTi?m zWAHIpxy^&%n+L~~xVZ1~@MBD?U@mA!mf^~0MPGf!K~OUOAYIYsi8at4Hgo2kyla5U z_CK?gdlDm@Rv1%`aqM8Cf?18nX6qPBC!7|)wM{G9K?_ZpVzz7x5)PCgz3|5k{GbBcq9fDsA7Bg zuzuvSS6-k+$>Rv`?E+0hG`1a0IzT1zUZuU9;j5sqS8hp}wqrSzaOLdSS{r-2C z!6R0onRl6@!(+sB$_`&PyqbCwe)fMT9kJvh$qZluK*E9u{LONRvXI!AIF+Evp|x(}YZY3Bg^$>obsetbXM)7g0Cn0G4`3gNo(8447#} z6upv`w*_GaTW<0|UEO>vECQGZ=g}w`Yt_*+j5=WjwAom1%IQbtj$jn$hr9xpb*l^B z02tiWURgG(M$53A$FT?*`TtS&rp;~S$kuTEnESb--96ila7+ZXn%&w)lkw@(DdYzC9W*uk_+ zq9uL4@XXeNjDOtXNHdGi!zyxDhy9GoNI}eiW|sfCe&G{6c;Ao4W!P1+^Ci`=fbwA3 zfyu{Zf~mC4cE5g>Ht@XOnQB>pIB3yoEBkzMEG1L^O8kgd4M0?KY0V@(pt6M1?46E4 zOJwMOdvM~z^jG$^h5K*jJ{S>2_+ZkAaWD@iwYNQbWe$L2eg~4pc$PPip2^yJ`J@J- za^1$l0zw>(|BRSrNOl+T+%&@=A`aqPY`n=fgprHtEp5Wk zZ<}6;8@4&qHbhl80ObKHxUNMlMt7+7Ae6*Hr+G~mBBVf!_Z}G1u^6oQyAm9(P!u?4 zi}l=Heo=r#xlnR8g>Wy!2H+}#Rq(?GEeG2adrz1a!XwiT?l7G?wdeFJ47%xh z1CC$j_Hu%H9$WNK94m4l8EbGL>KQpwcl(5PL2pk-(>L|@jE{v%ir&XS$!z1pSuDT> zw#NHdhbs1d;n!H}OtI*R71UAVRnvo0sr)Yt&*(z^xXXHeFz&o*4o7~%{g#6%y-7#U zTxftxCunJ2_~{S#ok+F{w&@YM?S$i8(M60A-4sNGm~E&hj%yqu^?<}&V+@UB0I>={ zETo4TV|r<y3CR1qTq&FyWyRKCG5U5#1dVW#E(PZc5qiCtyq*0GOCe97uW92ghW*s6lJwESU`u zII712LOQEFj(n#+Zzzr=FzyH&4NmLDTAl$c@(Om@w<^=00Xn0K_Sv$5^ISKrwE+8#Okr zYMvxO{u~dJ?4{L=q_DsaC}9Ny9o5UQB8XVc;DE?1hhs!?njIGKr|xe5vjN$m#AS~O zb5$`$bw`(C%+Ss(_5E(&?swg+pLYA`flfHoy$7VorVr^$vJrO*1D?aux%jmaV?90Z3YU5Hfgn@zW0LzfTVY)gLQN(Wvvq)||_ z#^qFr)et=3BOhAWCUpTt%QGz_vJ_msz(T%X3PoZlPAB=**1YCmuS#Weyxts96k;}siz>17Dq24u!y(qKA% zHfbG9zGXy27Fmulx0sXXlIM^mx7jOFuCnls>}KHxX51gp1au26pGkZUAo}AXT3LXF zl<^XBUkuq+owaS%w82o49)J^tYR*BYn*|FQf|DH8JE{znc?U!+Fr~yP08QXPQTwu5 z1;s813ha2GblAnOmKH+)ls23KB=;O@&4Byu#UruIW~c{p*HD2NS=84LRl6cWf=M&n8VE4PkHyTiJLFs9$X{a7!q4Rcgw8st2tv!9IEhJ1M>T11T z-PY6P=VrloMG$huj024NddD|7(IM9TVsMQacdAU~H|I-vTUg&r9~anD(7TD+DIf@O`Q2hS&1b{bW$BEgq4Y{cf|zONm_jiE`%D1D<0;F}caA`lwp_vr(v? zMd@}#iO4ees3a;nUgKis+X~Ggnd(DPO3CWDatmkKWi*tVP)4W4y0EkutETh$!+UJS zA^;Q1yC@iqn^(+Ohudaee?{|tnYMXj?Ws6X8f*=8y-_?x^XmHi z{PXqg(Rfl#e!i@}j6PStUE<%U`aC+XM)xP<>wn#!-k*lzR(Mkr&Sm8Q9uD}co8!ys zbF+Pf>)+7{1D7{HSD#LmcywHy9~*RZcRM=%U(W=GZ0wYx?F5=_%kz3opl+74h#D?} zF43M?P)zgyEMzUE$E1P6&>5i;#N?8vtbnek?_();q3T_a)1QKE+4pA5yula1nvkhICanK#DbGDVg80*wU z${z5>^=7uhXpb7sLJa{y(v8<5MWKS=mIfd3wxGjPEhMM=T|GfI1Vgpfa!tRx^>B_yR}!c(B6W zkN(;$xle4-#{MaZm_ofP;6`7B!Stb-VF1apQ>s4r9BUYj-JH2T)l0re2^glt8924d zpm6s&$D4CS0!~Rmt~#kIC((!+An5Gu?u%s!{}g!USJy|YPtz4Hxjo}o=U&y(5^}tn zuH(n2DV9J!7~SHWnae581s4r&$S!s-@VsIyH8TY$Ik57_%Cb7%W6!=&1FIwP%AQLK!uvVN?O=q; z(F02%g;7Pul?e<^{LOUj%QRx85aZgsZ5dqvGK|H4t>bt+gJmoR_MMrvgwq{K4G{Af zaNC4S!lvz%WX*!iaa#x&Nh%@4d3@X8tS>LEge086r9N&qCZ$OtZZ)yIb0~4=P`6?# zPqtG9fEcZH4&$o&38AaKyM)pQA#|A&x~A%zg_Q!Yq9(CGm-mq3rr-0p7|Qa66D*uS zsNZ-=4nbRZ*G404&oC_ORHX-FUXYqp(-`m=>**MHtl#H7d$^~jo>I!JSEU4+ASbac zBs3Gp^qC7>DLkc^Sg0!q{pK!O)>~Drm$zJM7g!j3MM{20w&BBuOp~b_EU94ldd|rU zBk~QV|9XC0;|Aw#)2yl^C?&T1xL8olqfh8ZflMHWM>OvOkE~+Kp5Yo79=OB(Uuci3 z5tQ6hP7fIF0oiV#Yo32XG;E*%wqIoh{R1}3kdhIe$9?0qHK;yti5R-VOZZ{g;UT7$ zo4hSnd>F*=A|tdY)H7dTgqzf`=f7EC-nfU4CfHx`Kwg7Ct-C$ZcTP!JntjP>|n5U;?v%se9!VHh=onv9!Ok|i{C~;w8%E{-p zZlO^{hx?b>qYv%j4x$p+rD*0eB55~j`neYnW+D#oyt2arx;7o4cK}R+OK_mL4=o|$ zLJ5i_@wBdi!WEm+)GqS~Op{6%)(xwIrx-=@e6-@T4vVE#>gS!FY(Z9VzJ24mRY~lV z&~LEkcc_0#f-RXR@d%M|jJH$vxu99(2jv^SQ^w{o(BS4N(7u1p@P8KZPcZ4DOCm+Y zK*%xV5K>135p}CRlyhhMma#VkNLJRBR8k&Rd>Xh?4q4wdpkcD(=13{b_?M@0+{(j@ zUv}d2u;M%8+A<5HVXWYs{2rAyv>0MudR|hVtMB>inM)HDPPiY34iAOAY^Kh6)=x0h ze9(|D)*N8yW<VU9#*;HTZ$K&bB zl&!Gi!DW%@jQe7PTT?;*B�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%0De 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 z

?r;KqnSn*V&NYpTIaElv{FV5XKqBx*2In zrnW^A)*$3l64qd*c3l$IV5atjsEBp*+65G|km?1lJME9i(%zMXwL3yPZ>Nah5t{cR z${E#ewkB5Tc+62)ww(2=UGYh?TEZHvC9J_(!Wyh4tif8s8muL(!CJx^tR<|$TEZHv zCG3N>gf&=8ScA2MHCRhngSCV;SW8%g*;+@c39+Ki))I)s@gZBAVkaGPJ=ab~Hd4zf z#TrD*eNLmax9s$HqcPecJAEASAI-X$Ea@(jv09R(xtShptTsl{hD_KG(QZk?>*(XO z8cBE^eVjJ4OvHlMyvJ*+CE*cvytYTu7m;qtO5&5W z^F*bryyLZq$(puYgmaHbRHkZHNyi|cQ?)UYTBhHLn5td3>9QV1OxGT*5aCj?;O&B% z`zk@Fh6aVr&|+2#!Y7CFwX>2Q#~wE4YiVnQZcU)h3bb`ZrR-C9e)=Wt9#IKv=J9(( zk+yyaB6n!}r4Gm6PA&Q!VRl5jM>t6knHbf;iEysJIfB?w33yIS^cK{y)U)o$$(gro6*#`YqK(fFSB zfJltS_q9#?P$x#?VQrtCTq2KXCnUu;_fn2&3GWI^9Gf3%Ig)T}R%vS`;n=Lw_SosL zxk{V6Uxa%Yd)!>DMIS)IBdj&@gqB5A4ExEQBTs5Gg^pG;C$%L+B?cT1C$*iDa6FvU zswCn0(JAe+q_UK5k*DlOZNFBpI-SxUNgejxQ(B|6ZF&_Nj%MtrK}5sABD&S{Gz;oUIjv`R^M^gXAYB`RXr zC(da;2Qe>jhj8I=;yJu4bqbmXBl0BEQ#8OIjDTGV-1le;jqiu($nY`yKJ zW1H@U;2JIIq@X`s4@5rI4%z8wB-7JQ*>oo&xt?`e&{)&CNL^oNr>}uB&IsL$QAXyV zFSpa}$OigFJKc|T(!D+rmThB`xT~IFr(Yx8^wp9Y$Nm{<)Gynqo!(gY{#1l(9LuBJ z^(;v%U7ezu>Z|SK8P#0BY$xBSmb%Yb5$+hwA-?*SbAlE_JNW7kiHhLPjo_%ZdJU22 z$?f#4&&ZPQHfXP}lQd&MWK?_oj-+MQIG{zJ3rp;u9rOxG*greyGtUd%@BNlV`0G`Y z65t5yug82Lbl6+{^)yM?TmAL;3#b#lH9&`lha?q5n)NXk1z~Ru(YM&CATmt9Cuy>& zcT|Kv_bXwEy){ZdPlUbh_lRhH%O%tmvuB27MRn4jNW#7pqtE_Y=&&!v=-!u+O5hU_ zE|FdJbyoyo&x+HHS8Zfu-SjPzurGDjy{-w}AFf9uLz9c*c z>Zk9Kgfn|T{WQ^SN1O-y>5nAgJlIcn`Igc%6!b2M>ZkijTHLz~2p)36dNJU8Ig<5k zN%&rlWPJ(IZAY9xQ}ha{!*_(F=v9*N9U&?DSxND|*GHx5cO+%>-U{@H=(Z!yyZv?Z zZIK?nU!%XioanYA&cXxqN}?iW&e{_-K#%?oEsIza--A&D^@)<)dmoER*Jnr?-Sp$A z!TMZD%_7UxA^K$^JaV0l%GCYtVl1<1_h6WwWv9dDVfqp~9XF5APfNn5+DGcz_afXg z@D_`adW@Zpn@8(YBt^lyBA(I3)&DA`#^+MhvvzXneLX72PRUJA2932-<4A>#(~rnl z@HzHz`V%`%3?8preh{f&X#!_idXAl52!2jqCkdZppQvB7)7;?ab>lq|uDk1!;K_P| zoz?_T)eDJmX1EKuKpBz$sxj=qRUq&G*elsbIcf3E&O5~ep--}5V`2Q!}mb?1mg>T~O* zUaP~@=jxY*rGlx?)%|{>T+?&&bL-W_T-{8%Vm4R96tX4#=5Q`}uD*mwEK6E^XlcgR)@Ksr>6-^<#z9|(0Tf5Nmys| z^o>Mf=VV^JGR)H}NmmT(yd+*<_nKZUbso?|UaJ>wzW#;O{i*kInO{%0K))w-57a=X z1@&~V>p8z;X^YvjI=pvUKOxDb!7B6X`q)2&Zm;27@WOiG7U|2SZl9jS7uD01>R(IU z9MF~4(=FETN!?B;!{U0nGTrM>k=`26mFeAyM2jt{*OSZjG}7Hru*J&tp9MMI8ayea zyk4u6>%U7Kwn}-u^vZQZjYw~%{(ICvbum)s+u(quzHWuST($@}eF^DA4Q|jw|HfEuPz`RVSA!e$j#7s;xS?LoHs}eY zD~7L4>i^Kasn3zRhgvU}H}yA&L|!)PM?{?TR*+4)_|~Jy%O*Wf;r4lAlfHmRjJQqp za#pLuoNdyV3riYtoAi5t1I;^D%y-HZpY+9ka=oF4-(+b_4NTgR$FTL%$Ra)YR z+pZ6Cs7-Hsz4U5znBI0hTUb(h+x0DyuvNC#tC#J1CF$U44;@pedraM~Piuhb6*F&# zUM}@@J9KyWjGIljL$4wdWq3!wMI?@YmAW5(xCq`wgJr1HU7V1_@u*VoKqP9gvR;X5 zby%WG-6AZhM3wq9Nm!yvy^u(xUa7B>Iy}nk)UQjz)OYHqooiFyseeZ#Qr}rG^;#XK zzEgi7EGhM!dbEou5vIPgUY+gK<49M`K7{dCU$;xouA|#kFWhduu#RqbJ>4FCT^-#X z{Rolh<$LQ{?$u9A%M_(G`={=_)P1L;4!?nz=Yn#PwO0PO&kO#j719`sbH@5k=+n3< zy1(j*?(072MjxAdvKP8BHV*HiMmMJNZ*Gj4)B9ZEJ=~qqy{!e>*Sb|oU1KdMC%Zh* zW@HT7)MO<617_Ew$)@^AFM-7)?rOc{BgD|Kb*Dek8ZTL^)4(C zXWu|g+I+BO;YmHPwAH6*>p#1r|3BTfP@+VxfgD1uN=M79CgJALYFnwsviqTrXax@6 zZRSk0OP^SDuMDrvgQyulyANkwOg62}Ey9Yjqq}}B)%w)0jW^@5Tw|atD!ktv>qJ6bQ%(HO+FIl(d#mp}( z>(++oCt_Bqb*pS#BaHvWNGvblRv3 zMp1`YxA1+uktqKk-o6Ars_OdxzGX7m2_zv2BqR}35J3WA5hErfECE7777!69lgW^b zOlHE&1QNv-x2jdKtt+nJZneLby4R&xwQAL>RcopED^}WS>r$##ty=%zbIyD7UKafM z|M&m@!zbT4_uTvLe$PGk+9zY@Y18C zp6fApyJvjek_j^FNvSD}&U2zY8w%eo)rGT%bABCWYF)hqxoxQ3koKCA5k!20`I?$n zP5iTGI;l&FB9Yv=mHPZG*H}7_Q^~)SRCFVk!NLpL+69?aq zVt&Io;$P0~;%-f$C7ekpiRQ%HCrF4Q)dqTT?C5H339vpgb zZu4`fT!N&N=&WDa`FHCi=X0-wNh|s!x||=Irz#tq_JJD;iKiG|!6OC{j)|d^eQi)&cMDj*@sa;E?hv2Oi%$bj#tSia6gp{X- zrz`rcyWKO#QapsVw&~OozU<1aY-!_YO-;FJ@s&Jm=bmrHUqT_L49k^-9GPl!@%e%+2J^Jj?O1=#kgSu)$4pGK3Lef&~knZ|CRNkbGM3a$~7wLy?xvlY9ayZB4 zzs;%3vxmarabCmr6VXqG`_{31`viT%P)7qz9>ul=<_}mmfe%?{vIWM2?>zi{VSF$2@%dw%kHLX=M@uf~g#~xf`Zr`tq&DPJ|`II~iP1n{LXj~gS z&zd~X9*XZ#G{$YI?v@5LI}e}rOnlvoq@|3wYqNUL6 zfQzOsJIR?-DQ|8Z3mLAfOlY~o^Zd;?GRlVMVJK&C8Nc550?y}uUJq2;y7~)iiEjBO z`Y@rbhYaU9lEF~>M^pVcoWZm_OTPBZF0{{Q8zYTD8Qm18TT*$fd0(C#S$Yf} zgCmF0)E6C;u3N4?%N#;m|D;*Nw!sQbHHI(#xlnTA4%xku!q9q0S411?W!B1^Fb4BY zi|?*yiFqi;R6Fu%q+A8W4mr1-=;01+SS={|aodfD=TJDJ!?pj*e zB+S<%^wTLRolRD($E~LP6USi9!PflWVJR)ESEmmcmi{2mk|uBKQKHq;3c|M4i?7)2 ziI(^eJ&uOgGg%sKyNXDL_l|Y!pR@{qMFRC{@S4Kbd_?D@^~+$Jn5^Q5?*ho$*W6)N9xw8;e}ZDX-aC<-Ckc`&r;j&`uR`l`Ri+2 z+C_S%yB>z;fjGLV$Es6PQm%E=sOOlxQj=8N9EtNnUeUN?cH3#i=IOT6x;Z4z#eQY) zQ;;OLIkU0TWcTE6u|6i&FtXU={ieB;x14!ITgdB5iBqh>?sSFYwkJ!@-K`+=i=fOY zVksidH?!`FB|%~)l)YWU+U*^gW3PziDM@QM?c~Qm*{Re{NV~>{bA~=8`)Gq}Po$;n zEJ&M5TmQdlQ>v4uQ=FES{%K1RYExR54VFR@Mq~?>(YD4*d8J3WX?Ll`SK7thhb0wv z8Hdv{M%?xDO=uZMyBdd1jW+L~Fk9*(e|L%T-Q~!o?T3^T4fN3OBotqY) zI68AQb$~4@vaT2Wg7qAs4XiTgJJN*zn%ENCSe#fh$@HYW}_o`wwTPs7DiE29j((!k%IvnmJ)NX{D zt(}1127HyR6@IPoYgH-u8^9mlehyw6cx~Xdf!7A!8pOK>yftbZ{s!=OhVmmeKWIPb z5aJ0O`wOgrDKfVwyUr z=yX!1I`_m<(08{I=UkjS%2PX*PREzG=-o@3IQ)aaR#nL%tJrr1(=|G!9@6QJ@)v+U zPn8=_;WJgMfSP&`h_6^7-8&iajxa^o-$r9 z(xyx;109_@O&@J8Y?!aVqt=vHGrbu6Tc)qj-&CH!D!oSAU9?so(6pQmaGspK3chdT zoewN;*{|3h^!VgT`5lDfRM}&vZ>&T0Y7sH~+P~(71=| zP}4r~&oD-t@$~t?_tF*_2aT(9R~erf^xls*5he#^^aM^d_NrTosVsDhbg$}IP4v$? zB<2`=DP)@VX!ZbNn1nK3z};<5-izYufiZo-;!H-3d)w zQt_OzOMf8!w}z&j3BIP)f>UmGjsLw-!{MLcl0L^c+MJiV%p7fgShNcG&$JVPHK{(} z*7Oa)D~fu6mDLI0uGyyp7dBkXcq!u!#v2%KVZ4)ZH{<b3DLFM<`nM#-$#C6y+r9W^$M49mv(i1zGnychkJIJ>5Hp9`xM=X+hrcsR_%F=d;VVa z$*OO8c4-fua2;^t%o{yauERZdm=~5j;n{8ev*1P0pGpwz zL9-oS&v=FF@G+#mU3 z-uY@?g|DYm4PTb}CfDRUX70vhpJh|$WgImA)Vd(!XmD0$oC(fyd>E7D zKoy6qWb~U$%fHGXxumI^xqb%Jl}G#=r5@8(kXeT?nyS;jSGf>$(Zx zDv*cenJWw{Sd7 zC3tTU@^)cXjps7R!%noq0U)(zn*Ns~pU+CuJ%Qflq0>Go0QXf(&2!)JlRT&}Qo>}<>wk8Y)mJ_91qm+-D;ZAHhF$?^fRM& z=p+L#;=EnPxI?Eeq3lA)kAas|={dXf_j9v=hvVx$yY%mzbY|MMobJt>?#-O;wVd-^ zp7C=Ub9QqoyY+8n-l-n*Tv(Ety2}%9#yc3n&%t}^gZ@o97japxW%O(G_TpW7&q8aQxulnApG0Edm+K>@KjLy7 zVBdF{^Dc*fmpLId8+XATL!3G4U9RVMxt{kiy^ra4xpW6OZ~K`3g2(j%t^tzU`RW~g z1KwEnrcQ5vIfzo9!3f<3w1=@O@1XuE`u|ofqw$%Xt(^LUCVe$vE9Y=4$5x~rpD{-J zOy8Ef3VbTpfcjwO!pyzuCzzKtZ)Ngp^xh`Q!~5V*o^oFHo9d^F z|E=Cs4+GypE#*Mwe3rdAzeqbDGBZto8M4$5NwO83BiqK{n^{jSJv)Dj{@qCzGvW37avp>NXbQ_suZq)(Yzsgbn2xS+z|85!>vt;Cy->7B8YIJRP)=AB81 zb9q{^zMgS#~8Aw|3E`HnRE`B#@g+cn&E=6x2-KFTQql8U`oAk?% zd@`NzveXKLBnD|vorP;L`$H=Ly*Xb?sho2G!c$xOjn@w=Ez(fKmD*nOyrWu+LaZl; zjOCamsf~6Okv6rfXcPF`wF?+&zW#gB4lQ$a@whznu2OY}L2pvH1HNC4zQZ_b^%djp zFqSMX2GYG>(mgOUrceyKLEk)Xw=pj7aRK;ynrBl6)PI)MB_xLKEBuN$5lF8$%4s|sh_ti+En%q}2nV+dqO}27Pp3F6Q zGS_4#hw0>)JGs3+!(33QJ%hZRSzN^Ve8!kJ`a8wX7#HIWWnsjyeN=+uPjC!X++J1u zY?V$@ZY#&OmGilk^L7rWdl9F55kIw+bAB1;_F9hZW`62sj%O>!c^5dVChWo&&2RK< zW&S<-BJJV{_vmFAR{-fg!@~%(UE8YDZ1@D{?KzI^IV%;)PnzC6`T48^tb@IRezm0Z zBeeK-tsFJ8T|1iTNsPse`#9ZwI^8b?z2xEv?{ePW)t_eiyNjqTDxbIQIz$OF)w^rAUsM|0}ssA1fp#YjJ( znFeVira_ts_1i?*TEsauy%XiSxh&H-_rwRv@{BW&oLrb^EcchHOoPTkFXz`v^rhj~?@HW65?9RV!UrT`~tM*}Bo z#{iGkW&&quvw@Y`Jm5U-IAE2w09dWn0PD0m;8JbLVII|_Ed#b{D}X06-OhA3(_yB2 zwUzKqXidO=tr@sgYX#oUVeaBE_b~kc=v2%hl=I(dF9P>ztHIf?tpgs=+JSFtL8Pwq zQ-P-515CwN0lX?xkHfcG-+Wj$#=+^JyLE~utW!L_`ZjRTe}TouW?-u4yTDA(CBPid zWxzbo4q&0@dSJ2VCSaN8cHkt>kARasKLsA`xgR)#vC{J}=y{A)p2t9AjRCCl{1Uj7 zafRnu&`q8ffvuibfhT%?4?LN%-SY>~ot^{0ZqGZwu;+bXujfNx!t*Jx-}5DKt4E)h zs9@O54_A%2E591IPhA}QNW#^V}Lh%DuK6qjs@Q3SpfWr zrxtjRXDRRjPXq8_Pc!fd57pSy9;&hDJXG^9dZ^A{@lfsk&a(z#_IXYQrlwHH%oGZl z$8;gn#VPbuHFN5ivy?e2Qm6%+QYg086srHXIn28p=I957^{=2-xb~13c0DB=BVKuYm2|7l57KSAgAKs)w+5 zFR<79-@t_TEnvU*ZQxe#d%$hpe*n+%egZt-`vveKuQmzou8rSuZS3RP z*w3|bfNSGzu8nuOHvZ1F@e$X?r(7FfaBZkmstq%hY9lq3Y9lk1Y9l9=Y9lX|YNIff zYNI%nYNITbYGYC=)kb9+m1SNUm8B|;`$O8g!^%_!WYKgBbJgd^PKS-qoKKd)iNLAq zXy7bW13XTB2UyFvTwMgZMco2i2TnD|P|MiPxQ%fq;~vKSj7p;v%NVN}+Znep?quA< zxSvt!9G2-y z+Znep?quAHUlW^8BN z#<-Jl595AD<>l~<)r{?o+ZcB;?qS@|s8TsRV|nUEq+XtSx5_=PnmIMhsbNk#bNtNl zGiMuf&ScJ+%-PADJ&gMqRT{_7Sj||QMm5mR^ftz`nX{AWJ&gMq-%6wWsC0_KNGH0C z>4{8NGhNGcJJSKCw=sP-(>s~IiRnE|KhE@irr%;(WpMl%96!?&nXYEKmg#n;159sY z`fR3mGJO-%dzgNl>HSQ<#k9)g_%k_vrYAC8&2%l(?Mw%l-p2IVOz&j+CZ_i={W#P6 znSP6DmBsOAar{hAWV)K^TBh5X4lv!BMLmBTbIxYYPNr{S`fV27KKv%rZ!)bdA-=YR_~lHOGhM@U4by(6{Y;<9^qEZW zVtNAtPco?f0+Znep z?quA^|SQL^jq|M^hfmP^u78AxbjeD%rR<=6~-E4z42}1eB)x{hsHg|e;a=_J~C3w zLUV#S$(&(UnP-`gm`|8*nje~9nz|>=Q|y`Qnc+Fk)8J|LoaEWy341nq&i0(|`GMz3 z&t0Csc+yg)rW~KLGG%>AAZ2sP*(q0~>`J*k<$;vfQ~r_i@04tBxp%VnIPVf~n|GbJ z)7#^XdAE4K7tJ>Y%c`;j+0wIKEI)Oo4ZslL=;>bFyOraqne zLh7GWKS=!|H8pK~+F@zOq_w1VrbW^&OuH=YnzSFKJ(BiJ+AC?lPy0*S!89#BBfT(v zX8PRpTt~E9tMN{~`TA`a9|GrT-)Sv-FcPIx|ko zI45IA#`PIDXWW@_f5uZ8FJ!!$@m9ur8UM)mBqJrWJhL-%W9FvJ(=)%5c}?a|G9S-; zHS?29m6eq>CTnum^sM@<)~vp)tFms%`bpNkS-;5oD$C5y%FfLmmwjyZvTR>=SN7)Y zOS7-b-jn^y?ANmQWxtu7KPozE^QhBDoi%FbsQX9l8TI0*yqs}4WjQrDzMTG?b91iB zxgqEFoL}U;kW)GOywNw0-apzqX4II1F_mK$jM*~goH4hK`N^0)W1b!J>X<){`Cv>* z?qRvpb1QR?&s~=5%RMvq2f0_~?#jJ0_oum!=021A$K1c>ev+%_dGoUJa`TGvO7mvs z)#t6r^W|;G3+E;BPRrYtcYfaY@~+9-mG@ZQt9c*hdGa&!$K)5~AC`Yqenoym{^j}C z=ii?HSpM_*ujQwY%^Q2v*g0dX$F3fG(b!wZ-Zl1~u|FUC%dszveSPf5V~v8Gf|7z6 z1$70h3Qj55P;gek@2ve;L(C73Z5@`x#0H&Zx*~$Fs-n%u&S`OaCPDO!cz-3 z6>cfqT6js}Erq)aA1HjZ@b$vC3*RsNv@oqGtLX5eql*?4EiF2!sJ*DCsK4m^qN|E- zFS@_z(V{1co-KN*=yyf`U38%6)1t46(#MS&ci6aTFg9USK!KW%)~_$A|8$G4B)IKF@Ux5l3{{;KgejlX^T zBjcYQ|MK`hkIyNtFK#SuD?Y9G;^H3{-&g!Z@e9TKi{CBQO43Tkl*}rrD`_qnC^@U- z#*+I=UM%^z}bX@6?r87#KO1G45EB#^VPf8yw z{YB{urTa=hDgCn4C`&7wP&Tb>L0LoDDP_U3jb*2mZ7cgu*+penlwDu;T-k5R{#NEy znp%u=h)R7?mJQ5Yf&+kRWx-hBm^t4EHn-p)k~(Y>;X6!k$|gFyiSUEzmgm(J^&6ZHcnxO;-au?`!G>`FJL>OY zr~9Cqt^TRzs!y=D{;8U;{)Lr-rd27gcD%~c7N}gDC@9nxstH=Hs?h3SsaT}iw8bi* zEy14oQdl4A@%f5nYMZt~U7$6n?`bR5by};sUTaf3wKeKS&983OLh4rRV&AUC)eo^# zeV5j+{tH%JdaY|}fd{9vPR&dQ-qw)~{NWVB2Tvhv%p3zed29jjr3Dn``?HDPwqQJP zR{?Q;HNOmaV%_1uOAE-is*&&~qX|EmI|KOnvf03OD;EI2!|_*Dk?+<Z|xZI9K^8erbj8WLyC{fBH4R($pIW z(+GdYSn8*I-raOFaLU{}fNRSB3;6jms{i$Q&z(SFHclqahPf2~s`Wnyes>J@la`Z+Q_L|mb8hde zd+#r-_Wq&B4gte#4Jsh3BY)Q?eD zjkIKx-MWa{SLe3gzKYUa!L51iGRoT%9P^*rDE>p`@a`t+QIc*K=S|YRejdfv#G_*w z*T4>zEmzgjSdiKf2~)Cw;(wa^u zmN6*fSLPk*|B}N)^?aF4q_@vwsr5^irGH*|DbhV@E|p~?O9>g3E12HcdJTO4|EKu> zr(7aGZ(T>dTBM)II%(z2Jj+YjMY>9l6=^AQPb8kq#hzt5QCAYD#5Rxn#(y5?mlJPI zwu0nJW|J!$?*f1Fgr5Q>55uKvA8QV`_#Qx*`5Dy8qEXB$q1i`bUR6(hKt_ER(>})g zDjx-3QoOS9$z&=L@>|U>fOB8v-ej0B8z`O?GIOzhB0W>;Q2L(a*`2E^mh8t&^hWZppH|h*uzpk!e5U1FQASysA>3p&P)gAd7uu> zIRl(uo0;Ie0L0xtl?Bd=W;Qr40d@5)XyU2rH)akvF9UV;ZJbTT?u|JHoL7N5H1Aw! z@OXPM_`fytz<&*>t8<{WV~58a3(j7kuFi%2o~rO}cW_<@;+!XYn=LbN%Sq8ejt}eqG0XK@F+iTGJr+~gvO$B`wP**$D zG~m_H_VKm_==!?47CQmBj|bG%_0alpn+T|@ozVX2RMuS3H>zWSH(?E+cqOS=?!wstx2yV{k&3$-1%jN4H-PVJe*k`fv$&f2J5W~#wYPx( z(EbGcQ2Pt;Bb->qxYz#%9Id|x9HV~##Cc=jH2p*1bp2!CG5ROK8Tx0y3jK57O#MsX zEd48BJx&PgYMG9sw`w_1$8NR}qL1M$w5HYr(Z}@hpnX7H4d^An)AUksP6z60t6mQJ z3?QV9J`wnB{cv!$0nyX+BY|h>Q-EjdQ-K%h(}3IcV}O_F6~OQ5vw+v=vw_#@bAi|C z#{#d{j|1MI9}nEAp8(vY*8p$SYk@c6{0GLkz65xSUJtxgUk+Lt4ydc=^*+$Q2I}eseGBLpfp|9- zogr7h0qW{y{dC|f`WfK73Pdl}zYY2|AbP2OCg{CD^iut7(60k^wNF16^cz6*M*Td{ ze+QyB>KB0i2N1nczYzG5elhT4{Sxs13B(;F{rjLl1?uWE{Zin+^vi*t>sJE5(02f* z8rJ}iHm(CsGj0G*H+BJ!F>b=MGk~}oY}^7|VB7{g!MFohZQKd0F@A(F3xViS#*cyP zjorXgjJtt8<6ii-10l7H`$2aAA+?MLfma(30k1J00bXnT9C)4a81Q=Iao`Qclfavd zr+~K@&j4>Vo(0}!JP*9xcma3^&OD>_jhBIU8m|KHGF}7z$k+?~sqs2eyc>vCHr@bz zFHpzVuKxh~exQ!;S-%PTL7=W4GTs9HFi=;I7=Hr&C=fEu_zUPgK*%)X9nil3qPG}- z1N{UL(#&`d_>A!ZIKKj-HI0M7y~c;Yw~UX02aHdEe=Mmz;_G{ zZ!-SGFo2&L9^hw&7x*tD4fwf{0sO+q0vhHhplOZ|SqrQ&7XcTVOMtazJ+RJP4qRk502iB$z$NA?;8L>% zSZ}rgmzisT%gwdG73Ml%gSj5K()0lvO+T>J3;^5AAaJ$W1zcl>fG3(8foshk;7Mi_ zxX#=JJlTu`*PDI7Q_L+upE&?*H%|xp%`<=<=C^?X^Gsl;c{VU;o(tSyo(JqPF91f& z3xQGdVqmX%32>A7ePGPI6c{%z2PVubfqmu<;AZn0;1=^bV83|-aKPLJJk7ibc)EED zaI1M6@C@?~;JN0V!0(tpLY5WI1pfDdy840n2C&yxYn_hcEG8Vl4_foBx3&@&pGA|U#+Cl^@j$p_YX3V@3|Metn=M1S^-2QKxL z0P8)a;4cHBk9o?09iEB6fah>vr{_pu&@%t zGYgy^Ao`(aHs~l2{m?TP^d=zsq32lOcF%FZ>pjN!j{__Aj?@NG{6@UNam;D?@7z>hsGz|@pBV0y|LU{1puV}@r6RvW+flwkkhXzyIy zBb=8}0-T>w3arW~2QJ8%2&~RH9Jny!NMK#Y6x=RcijZw;8A7(Hl?d6Unh>&0H6vu3 zYDLI4wHhJUsCI;0qdE|Bjq1)g26!q$u2ErxT%#fx6~JDE^sDO;(yw+Rq+i{bF$;Jz zLi*LM2pLk(!8fFS4d0M@5xyby8~BFQEAS1g{qPN|Kf*Vx{)|+@>TURj)nD27U8E9L z?<1AC%FHYQW@nZHb27_;V=^ZK^D++yj?Fw0SeQ8ltIA@8OsEM6nNVd2nNWuzWI|0s z$b>oqAropcLiVe92-&aZBV@l?fRO#F8X^1DLWJyBbqLw779-?(@2EeGnvpXvCuj8J(N~Xtb@a3`(J@+XT5f)BU+(X6^|Ae9k0|IVxUV3! za6;ks@i?|tJiGXW;#l$Z#kUquDG8QDOMXytb;*ng^%KsWaKnVZP55v^Rq2VPXO#Y+ z^v2S{vejijDSM*qcV+)5Gq=&bS-P|P_l^8po>5IM=ig^#eYnw0_jzgGP5ymWb}rtD zF*MvmcsBs{jqr36{#N0y8GkMKYsFt1{#N5}4eSFa;%_bfPQu?h{GE)y_4qple?I)R zR z^n+ToPc5tri_{kEAoibiFn(dYf9Be`ETS<9>+0 zdhM|B_|CjO#oRxBo1(u3CHI;imcDNOxhw^i1utwN>9B8P;4c%coekb7{N;cijo&f& z%f(+F{_^oR7Jmi!E5skFEEHdwNc1)Y6W!6yiWzEkFcuF*BcL{fBB6M9eI%3!`NN^p z{0U)KEeR$TMf!SzF@GW&ThQtA&6=UAx`GK`OE9n|*wGRVYz!uvV$no25Dmxe0Iji5 z57Be2z_ron#!!&GiM8NZpRZ!(3?IlqcQCNgdA=j*u^`~{)rI1{VgEpF*dLG2<={iP zGiOv#EJN9|N94{OkvntdNHNSBky|l~!y@~$n5yaQYzf9ThXTO`$hq)_=4E8=T#0E& zA?GFIpIJ#Mp-~bw@w!k~FrJ_kggZ2ain$zkD0|+B+=`jAM=GyyYok5A;b1~aGh(=k z$`RRfN92ykoH>WmkM;&5RHBMXb`H(^$lQv#95^0Kv}_3mds{>3mt664DazhhG!Tr( zN9vfh;ZQJ=SQ_+q$^#X%`H7*aR?HcZJuL`3J&Le1x;gZi)UlgONiG~n1mkYznph|i9LBAUMj|MhJ77&9 z5bPbq>4>460e^fDlag-m$2W!|UCV<5?g%J(t1=LM!XJvnTenyP7IjxkWw-om`V!sf zCZT{|MgY76t^omOYqTX1LmCZz;Y4V)Kip?OYJ1q#p76&K&A~u0v>Bl-W@leduSIT# zjO|1!BlT4hD8ZQ1SCQ;U!MS}%c%#D)dmw=NNCayMA+@EK^k8iGAxJDI)tDsWa45gQ z=HRA2OWe#*iD+{$-it(nQgCa+aAuAQRn^iP4q0PMssw_6PK9b&T2rAUBOr!}Xbh?m zxEK-OM$kbQ%!Y8_qFYoL0z-H_eW)sG*};7o;fXo8Ux7J7Zv!_ztdYR98wuosIOkxP zkbqciCvV8^S+hVb?u!KQQ*X_a9pNBctNMa{^jj04nUuV1qv0?*x41~q`eUIudo9{b z!pR@$34K%KI~#Os?j&ruyQKdGKfOP1zxu(sC4 z^Q67%BmO}xcI^M8xFWy;{53>7`@+Em99w;z)t^CdA84tquUC!BRm(s;5$u^>-w3@M z;<+#A^QqM#ziNv4yL$W_mgZt6TywLEK|ZxzK5ExHr~519PLoe!Ol<& z0&zJsIZP`ZsQds|*UEsh=BfZX%Su{&gaUSjJ5n@`j78XMp+AlQzCLuTE(pe8EZErs z6-fEE_(KU6#NrGj`rR&3Hh2c*(C}{N2x0>qen?ZXQlVOGg$fm6sa7b7dm>V-Qb=e= zv*Lz7v`)qGTH2IyX;%E0OQ+(rocT=+nc48Dqd{RnCv_*aaFEu7(c@O4IF%vllk$-o zsp2tdlm>qU%0p*sH`PjgCmJj4^U?AI-6qNOcXs+3Ba~gl*c06xv|TiGs7|S-SXLz+ zoC>oSMLPM{7wGPbY-ExitMx~iZjB8zV$HzxLNt?5QTg$qBa!YRIpJPZ+ZT&LLRuUw z)D&{3s9PeTU?+{W-e3T80O*y`1O~m(E1+2>9hl>SG0cz~5s1rxHm{}Z!A&BUdV}ru zQHMx6c!(xl0qYrxqdrbGZAGvs;>Qr{q|V#T3IIqNkkP&b3LXfO-=cnGG{K{Iu|Gtd zCKNXs=|ts6BNmHdM!{+^UnirsT|UZ~cwqDgF=Hc08)`u)QhC7eA<9IsA8M;Thz1WS zt^ni(gH{S})7(nDz;ey1q(vq}%JOR@Uzxy?fu(+64(K0nDdQ+*G6ZC251KXU_63qD zJG>Az8>=pDab#9vxz}Mj-yc%$D$-a&xE=n(HJ!NB!kQ6a@WUQj*#2I1Ca_q>whP8+l}6lBxSH61B?1G z5ApDUXb%Ue4&b!b;Uayks9GTSK~g0CSa5MPW-+A3t%)50$U0@MF)%0{zR%V#kY!$; zAgm9aH5lO~kc%1jL$6RitXaC6MJSLkV(HMS8X%K86;}f5AA^(;C1x?CTN4gPw;-T3 zNxJ8p77`j{A%nM-dC0ZySTy2dwMG0`N=9SM@`WfxXr=gF8Qij@55K-H!fr^zU~F+H z817VLF~j)f-xk=rx);ZygBgvnh0u87Za=9`$PM&CR4InJ!jw-xB#YESsOFr>xDqnZf%9xzz ztvHqh6Bgy`VKXGhiZK`P$3(8!Gz~vyQav}p1h^$?g-AM}Jz4D5AQFPHFc74Hf_bME z`eD$ShQwKuwAUdtO;FatNx@_2V#*3Jj7_?2(u!b>;Q9#GC=l1GeuF)Cw1mXn7U{93 z7U57~Rz}H>sM-jV0r^2Ag5TN_?W5nS1+#pxwEtp{XAixj}Pi&5}KF*TU6@XgZ<@BNK_OpvIKcNPK zP(+;0LvF|qASvf{n7cqOP4t81m6^(-wuN zEs83lv6mlm1hvhGiU);h@b}krN#QM$xneZ2`c#AEN-8%t1DcRc(qLgj3%30>6i=}dY0=aW=+R*kBG&Z6HAr(1hE04!h}1Nes+Ponh~ErljM*;B4_O>Y zwO~HYB%Hj2axbcAtTv$;dMMh~XkE0A2x~RgI?&6d0SCdzMUw%Q0UXLIFD}+#abxk= zfaG%X!pHK1WfC*B^&kyT9^Xtrr6zTn>4b|Gqg``L4JFIItj)GYnMRiQ*SCQp!^{>9 zql!SH>&2t^T@gwo@Z;-*X)-YYin2_)3a88jaUfWk9$BxkB=-m!kLrw$#KqtmiOmWS zrz2ZcjlHBaGXv8XPr&SuxtK}H9Q4K*J5XBwWu1%cYlDI$89sEtSki@ulC&>EC8xS# z!j7CBBoHVic5oCzbt4_7ELDnDEo|vT_q3LpwwC2G8Au3A;}eBPSXL~yIwVXPtt{FH zg>rEuNE5oxpb!qj3IJ7WaDXHOV=Ayw{F3wNprCND>MYC+WVqz+(t=`utdC=mSnCvv z)q*t>WZ07$BI)4K!)ywJ zBE%#R&2tt>VH+bPE&QD;u$+>0I8(lWEIio(@ns1VcE&`wFb$#HK~FA;0-z8`xoNPw z%|Hz>lK7cPd9gc{go=d*x!LZS42r2W3 zQiK*sLF#E&kN8+*^KWZ7o|Nf}A&1FNdLt|7u3rW zuKEZDAPlQPf4E0z(M~ZtVpbZ|%b2wiJyfh=&Z>Yts1}(RDZzSPm(^07jk^59hSTX*HhAqlm)7mB!e8ZbwPH!Bvax}I+xIpvGO4~O-ha=Umh9GM>ASa${K*n z=&S+6Ne@HmLi0ytJH}yKs;=l4c0YB^=+k9#hcJl^G*OKyt~ZDQ5-dU0M=O19VG=<@ zHyXs);Ie{nNN5Fk*v*x`Ka>k8=tr04btEDm>9L`Yprr12tr$jN*^v&j(@ zp-rK^hVxms8qp`AfqMGw^lH&7QwQ~4uUG=rjshcve+)}h2%7wqWk>I%kSBi+K9o3Ket3*`$ZNl`v( zEenizvp^NHKGI9}8CVF#O=&tkO?WhEEX3AWFeYwaB1UFC%OwqK`OvDKbDZq!;zSn_ zg444ChbV2KDOpNp!197Xu<5D=6TY=Y|#;;#ip9}&>D z!xy(HbO<&{W9vdpaN^=>@JGbKokK|97TU?cW+`@(FzxY<5=N2yQVytI*vyY+>r5^J z(YVxNq*f2*xmwZjLQJbe@z9{)HL%@5`yOZwL0NTzHg=E>n{aWG0cio=D~ zB^Yzqi^Kk|xZ{+nUMcS0Q!4sVln@~ zNWrn*EEKwYKKPl7tJQr@^x|I^2s4Y9ny1Ww|R3D{(7RR{Hi%6FMrV zpcY9=EQ454;{hZT1ep+|^244h4v}YUDH4W##Kf6}p&q*&EQKU!LntDIy~GjM(1RM5 z0a82z)HHk|ft0qz(U`DlN)-zebhmht4S~BPNRv@tP%Wb43UNn5g9QtU`0=WJGY6v@ z3Ps@QtC%q(U^_@MH1_&YOOQakXyH1dO~INl^mVQ$$K~6Y^q?o};}VDhSfnj@#J)!c zghuEZWb31-L^Q)L3xgC(X(Hnc!QjT)*oqB`Hh}7}Inv*_L7@)=8#ioFbd~|n#KIfY zqE>$w;)NbMXgt%9!zv@XFw_M#)z&Y$G>dv@xj^eCE{@CT3p!rZcE+)$RSv?t>d<3-XLB-0xuDftscj@+0YNv9)n zjX%b}yxrJK9NuYU$`_|Yd&HxmOSyZt}|rNF%fe-0mD6+&alF^nxV-i zaTI`i7yaT?PnS%RVEJQ#ZrBHDjoIK&FfG3%jbTzHo3ZL9SRC#HZeT=^Zcd_!2B=yB`Fi1lDH!lCCgAwv&wa+8}<>YopaS}{>lgX6Xj zq}TyS@C9R*t0ib1Il>SY!;R%($rAgafMvCo^83WNhJEjTD?m?@@`WXgGl?kZtT^OD z^9i=`VnJFWC_e!e8~q=%%Ronv2pBD8nj{DLVm`3CAaP*wA+!S}b!a;@`aiWaIl0?8 zPTK9Xy(nI1@R6y&9*C4ZtXSxIUmqaaajIxAn8aD7f$<9(6!r&c_q~H$G!x-qrqjVO zP0~qFGNjvKD8XUNyAi@sp1(;rif`Bw?GW*OlW?%X*elHuVsUNAkKoJpI9KsyVL7z) z!*Ve^7&lmQ>)$%cH$0dtH^V*2#urOgJE@OJhtmB2RO57RD_wZCjF$847faku$| zrcm2=WiWxQQD`qPl|sitL_Ii}(}UlY(HQ;Ibl_M35t8C!nigr7#p2jArSf6Rn&(X0 z_F-!|(`j}LgnIEny+c~(uN=l=$TmdG8140~z#dj>w?EPtJW^*IS1Zm{@I%dH;pGH`;#wva z3L_j_d9YkDegNBmII+z0D%pc**g<9M9bkga$6^r%4VgAiv9YKcBb*qmM`~$R$_Y9y zs>n64Jtn=94hSj=4n-LKjVR>LCNY$jAU80m&}dSwk%h!%w+(g|*tZo)v=*$q6&7k8 zY?MHVR_u006m9cb+t_rD&Jr*S*rq8Gw=A*wzRgw^*uvZ* z`L?$yESD^#Z9gb9OE6X~zxt$fvN^08B^~w=kxIrb!kiE zq_}sPWl_}}CpH@W8%PFPR`ZpKaL$(b3(70qrK8wDT6vn2Y^!~%nohLEP} zgayXQ9}OrM%Q7}B!Rc&hrE&+M98=oxd!&wGAGKyjYuQia!UM@P zES6yi=lc)IL}A01JsS7*8LNxajZ$Y1Zs=x<%ZIk?po;dnLt}oiSLv>h7;mx>>f+%> z32Mo{y2A$)?JX?qO>YSGyF3xrz4aPfFB(_G`? zVdfaj5Nnq!ge6R!6iIlw{K@d+&Ws#Nwnw8qI)`&{u9!^l?tq9%jwN%cnqcBcj_#xb zXEQB!3pz9FY;r0?tqDbD&BQS!-1xJ_j^je(I1cMVf^|OE;nRT~`wE4N3q7zoh-EBH zSab}^ioYh-#fx~TD~)n|%JGv^M~<5}DtJB$+M7|(52IUPsTmT;GK3EIwUZ;Q)OiLo zcjE?8yd35*G30nzcS100%RYhQ1Bt;WQ+a$lZ1<|c;fB!SF~>fC2%b=D3qn zk;Agf<8WY2ufktn%T9tX(AV z`8*F3)`0GXpg0ZL^@+BK84=&dA68@fE zxiNqm1W9N4*>_d6S8c*y3v7yPSae+$r1jCz^^z@eq2z9YUhdyg8^c8w*bbqR;xJ!K zi6V^KIM`>R6f6oSba1rWazIJ7?v#>uVxRs)jg7;tJY3{L1*(hqRIy;W>NA%4B@#a8CS#LXK28vA%6r3Ia1rHETTtTvA5vm}@p`H=wP6;p>D2}Li* ziDN8fY#NI?I{OPIAEa)K5lt7Ugldh_87iT%S(c=6(%j0k?9_9d7Kx>Ln0GMPGU~ei zVfiE17KNUkq^&i)wXn7V*v3TL*&$ITg{{?ca``Ie8p5Kt+kDI+O;|N0Y&T3MIL^Sk z0Q8GRRwItfNgG%hz&=tSSkIM+D^Lg<6BEEtKGkb&A9g1A{3)NewPIlX8c(uL^0gs+ zShdPg-64-!S88}Sm5woCvtPlYle`Q~aU(-?2!Q(!nd2z0WQ@bFRi$jkqoB@9At1q$ zFNhHFC=uA28)rJ{fUHW==vi23C!L~8C7rE--f{4sS0Hg^~4$`G0|3HR92Xb*pD347&IWPH6)X){l(-U5(OvkW|%|}*Q{DPIzzDVinacoqh{)mv24~wM2Tcm3>h_^{t%t2il zkJ6FME%>#r4r1pOJo~tzlU$NRNj3OSjY^`hMSyC7gyD1a(kX^ofyK*~$Krxz6{ZfM zMIBB$VM?Hz5XloOl5yct>ZBHN0AG-Qh$kisEGlS1!m~66hVm?Xh7*V$6tz-r10|mv z#)Lx7@5^zXm2L^y$q!*4T8OyZ7NgudceU-Q+*yhwugXrhgF$Vta+-$Q7REyu5-8mT zbmjMdKR#>uD09O%h{KhaAql#54eRs|ibzYv&0PNJ0jl)r0si3|3UUHiO#JYos&;ip zEGC8opa%Qe0M?QSaW&v-7bPb*39QKR763`azG2R;dgsQu-3WCdKhA!_ED(s3P6P`J zR!H(}YZTUUo5A*c%M<%#m~Xe>K2%)s0S_F+s_l+q3Xj{-+RJf1?GWV=IFdJM!X!PZ zPqRN&jMMHmmv?m7KZe6#*qFz}#nWXRhB#&*q8_@1IfN}^Wf)sD8;5P(UW&_>GduX#&#kTkRJwZuh`IZjc9&@b=;6B+d~LU`m^5$?ePq~jP(b+UsWOQLSPUd9NZqNncg4vHNu{nt8 z0x`wp*B-!@X=b|Ug1C6r+dVLS$Xhe4Rv0RKC3R_kmuW7JEgyOV(;!we^i;mePE#rJ zM<+b^uu0HjlO`zvc+wIQC6z4?OdY~m)YFS|b?8*$q-blfEk~~$QrLA7O%R54W?(uU z%s>c9)D>bdgKs6&#N~P9Q;3yNxP{{Mh3w!okVPLvH;!viwOiC?VUVBLji};b%!UOC zR4eO1SW(B7TPb3O`xqKC*ec|DfTktK=~^f_OimUj6yms5Sn2a6x28GVARu#S`RC`P7E zm^d9i$?B2#micLLV2Z?Ti+7?)n(#4ZBXK%+!`C=@AUKOMcFJ*m7n8CujOh#UseV^5 zo|a)?g3+3JIh` zMLb!@N`y8I#m0Uul^jH}J?ujy_6*2ce_VPfA4SEXT}lC#AJ}`D76C~n%KCU524B=1 z`W4&rS))VBL)>!Z)>f+GZ0{kN8%JjqJsI!PX|jYUcNI6y;?!nPml?zmDQkOE`Dw;; z31vuhktBKlGD5l>z1IrqksxRn<}U( zh}T|PHpZ=kpr}S-JEB^iBm)m7A^RAOIB{^1q%M2Gpi3dBNO&Va9HdLmTO4k0OhD8J z_@^ckjSTce`=A!#B2-r&QOI(#pNT~fCm1Y(V<$;xxmhZ>+@hV^o@`F!9FhHAS}Gi7 zT9$M*30%UDChGCNQry7yCoD6S*g0rIV$sLDnM#b!*5)aK(-3pUkj=me54=BystNb! zaoP+wuO%cJegtm7^2JO|(NXc}y#(@39GG+HT{(DdRg#0)Hee^p3K>}vOt4Z?&lEZi z)-%LQ4sCK^&xd}T{xU*8;X4-o779&<5+Ycsq?41x#Dqz|xNyNvj87O^i45VuZEv@~ z17sI6pX_ed6=lcGD^%&I5O8dM#jAPj;~IAET+%GLN`cxv9a^N70*>Jli9er3`l7-T zwTJ}^0yN=HUT}ltij;w*gY^b>`8x25BBog8DLRqMN+4dg0*cCG9jsUNHFSUrxxg`W z=|?U%<;CsKt6^So59Qhx05{*~cSVKqPASk$lf^yqTN^>W{k}8G0!*cr)Nz$dNVx4dM2Ux-5 zSnacc?5K|qa?(P=E`v>@;|X(7WOE2_(%=AAr*(G=>%5V>dwks@wpK1;s&5#R6^s4;BcvBVaLM`NSmz_F|}>BNJ=u;?v`|scMF@!k}T`cq~i`+lP@;MF$H7oN=U_$fk|@+i#nc7 zAlChzLQ&j&D@2@Fj$%(!=q=C`EpiE7CE@Rtrz~j7*>_evaD~2!`W=NXG{KYzaf{4rBtXtG7pKvy0K$BsrB7glIWJO1mryN z)}OfDCUZLs&tjww(|HZli_MoZA#fs_I4)C;B*xS z!ZU6sdAIuUrWCf{aQ|>QeHaC{mneZaX5rET*~J>SrM@=xxqab?Mrb94GnzOw91e%# zVuQ4KR@rUm5>)mUKkiJ*6ZWg>)gOv-4qdoR>=49Ei0g zWKNV^ByT!fZr2V`vbF87?i>vBbTnc+I4$70YO|eZqGZyu95f+r#uhQzqv#@)MX_kGXqXsq+zt1LEVsKj z7RxedDcpZlIM2nmL!I%!COF|i_3MSki=!3db{4ue$64>%965*};r(>)RMns_lAN`= zC3diI%^7P8ng{76q+&r}ihANAvg`8ONqIhXImwsyd7J>eLa=1K+)cycQTaMx9G_E3 zBt1gEB2=Jx@RezXJZ6Tp6(qdB%S5K6$8PY3`LF3 z4t~WOQxt~UlV2RzV!aIy%X!4xK=0cl)0Lmj_=7>WCu$=2l?#?c^8E=qQms~Fy=#qj zyt>YItfHfO%%(e9HIYtXKoo;5YipPJiw2C0YVvix(iJP%UVi}XPiGAmS#Q`Tx;_H2 zj-5RCP~qC}E+e9;RJ78egPzIiWs{3#l0!>Mn@g`z^|G68J(_e_ntaktX`n-e0!a=` zN^C5(G9ao%l2wm4;D*DEF}5-*D4jgBL0zJ+ZuAe}n;zaJ_|{6RQfW)^TZ1pWSL-X{765k4MX2fq~@ZV^7vUJL(~_`e8WYJu;lMtn!6RpG_P;I-iM->vwbObz@H zwtNk~BeMqIS*^i$T@v^nOc39GfhvTr#rW|XR9aRoz62A+*I`0%sr(jT0^!|UZCwqX zDuYi9|D*UTLri{rF{TWz0RH!}PY~aqDZ?MRLU^JJEG@ZDuS(JW=M-(`6hp!`V&KaSi_?--D3UaU2*T5gsF#ebE_l>B` zJqQcyBy!gU%BiakxG0vPX?9XQ)KE;lh=+0-z*l$5kRM777mJXBr0c{rwB8ZJ3H6A3 zGuH`~kIG3;QVZCv^|d*eZMSki*GKO+OLa*b*GnJrPPvcaDZ3y1Z>7G9%0E)u^&(Dc ze=0H6hP%C;l8sOw-Yk4;Crv3&8R3zW(PC9lme`Nz2ffabEIEOX!v`!;0fg=#P9M}^bYE!Fp@Jxy_b5TA92w5!1YQXF0QcaNPHJ6 zj*&wnpX7kJiG$HF&5q0N3B%(#jq4*y`CL+O#gBU_$ZH?^D~%f(Pm-ezNP)%>)5pLLIjf`H#4y zG1VcZDkEIlsU9IVaP9V3e0oO9r)SGpE>pRs^lE%FDC(8eQg*2P$WJ|)5 zEZedz0m2rNY$^C5S&|=%y!J>MqlsrMTMt0QPP&D?!4bi>6>LHLJe&)`|O;0BfG58$7J21|hp8dD#NXBO!?(QnG27p*SqhhekoQ}?i8v{UVo;v3XP z`+OLAd2frA@_3jNKp*Qe<~yWaPGxDJJ1}ACc4{1SZn&nmUuu9CGU`@2@?nSW0pTH& zzb+>(+MM$PyasTz^jGn6_$~*g`vo!f$8|#Np(Xz-!##}LV0Ew^qSNAhFD-d}XbCvr z@`e2mm!Jul>6xd)EXw?%%{q5~EL;$=*=lnHS&9;E<8|l7^@i=Fq!Fn{?|F|tvse1>(dSm{F?vebJlG*A?DY}Caxgrk`}Y{_^#>!=7MxqA zkC=7tLxp+FYJCb^&&_Jp_NU{8dOVYMuAxC}9#!r!Xu=4-sT;>ckh+10C~Owk5Ueve?p14En8!zuiay89?*SJ!VNaWc9phBt;aJw%OKW>*JRKlt-Z9&#>+6yu(J5(-YCImA6>j^ zST}AsOTOV@;!)y%^XGcOS{X0#$?4kk+S|e%r-J6p`1STQ3(xXpil)NUsrrBA*q&j$m8ZiRe>9&JDBSzn z!PVdQEJ_@B?Z%pGzRnu&%B_-aJlvSIL+%fo9Qs_z$3R`{wl2;Aj}Y>0VkG3=R_wRi z^7{PzoUMs~4{pz%v&_%v%V^dvKOG12(Q%>uC@A*22xk`9d>6e$TOk7BKH_?yIAQ{9 zZXwMa7ER~&W3L93)62|{0FLHcKVjxZ;&ik=D=FLiT8wmtIh|_X=;h*#+uMEwK&VcnL09<09zADOvk7&2pqPB*TZkCkQsWg9)6sJlqeA~|x{5zP z8~ut~6bas{TprW;9&6jnw~VUf)naxLg3LWOeygu1kT1xSAwPbhZ~QC%zQ?~l;@|5# z#o5sBslnD5J2-9ADCXv@pVN8m{rzQJjpL{O-kaWP^z-rJvnXWnnz`h{)*B&+r&n(l zlnCo9Y2+_^zVWVcFXYg4;c&C1eodnlyx7n68`EkW)y_6O%clwy*JYo+1h{k6wF!SOpbEtbI z!WG4AW#d#cO3g%0{5WE`S-4!8*T8oP3w=YOtH{W-XcvioeI?Pe(aI`ciF8G+d)BS$ z&TH&+7`a*6NnfPY?|oV=v3uvm^;5LOl=MudJHkYy%Gtz)z4AGs5y*+_J6)1T=4@Ou z?2*uvt3T=txLGoR)c48%ax^~qE|&l@gIMH#$(D4P)pQRJqktnVQ!OL{M zrQ;!e^m3ZXHm7s12I3G zX@#Y!#3oMu?xJU7;_rZ=ohxB>}e^n3;~n~TVF zUUQPua;c?FK0=F_U?rVvy}eq?m2tGWTF%OvUF2~P>sP1Qb24*_AnBLi=)d^d*Xd;< z!g*Gbe3x?%Z}xz!hI^o|p;RB3o4-G{@oX*5G0A8=+b$KZhjV8SMXvd4w)3_O%O zjRHCpG09JmsKn!jplGT{-;)HtSK}`2nNnx#R+u9F5ypHqu8fVj_406KSgQrd4O*_; zp!1bFU#Bf5Yjn0IT&)Gi4LVz=CmZc-qs}&jw<-y~Nq5%j>aD7MStwY7*C`6^Jq_Hu zgft=gOZcP*vGSL!5tL? zVe^d!fyewz-=k{XpS(XiI9H2bXRMhn#X$?UeK5azv1hB{b&`s;`o)|_szHy!2s0MP z5Vy^@?!ZGFF@o;M%~SK^G6nI3(R+j^+_G$gEd=b)H5`QcXT(BIR5Wim`LyuF0Pyu| zp5Blquc;&z^UxNV%l&F`g7Q5$O=Pl^n?u`OhRj-=T1E&g!6%(1THIz48p)W^79Drj z=aPG`V>9dAx#zX;O5Lo8ozo@Cs?X5ak0*gVVV64Q2MD9scoYNoj4!@jGgAGP_8+;F z$5qgdK&9@+*^Lo*cn6(sieh&p^)D9TC4PwWX6LEgxO9RO8QNE z@*29n;HNEcSdoSg52-$PNRp5y35C$Lf!S7JpA*^gf3JUl)gU;`WHS02SrF(n^sobM z<~=x8!gn%Ks((!H+rxXtz;{7=PwV^@k8<0oM)9QQ{T4nU1=+@fGmj$ZEg`W&wwmpW z;KcbG!|l22wei}vk**NEuZnl(&*n&If0VUtR=Hbd1X0Ip#PqA=4z#57*O>#bQ4YXn znSryZ6Z^YF!qfZj)!x~y$y0b+CYT7(3c-dTF|85C-s>y+uGe(0A4K%Fdta{>o?fh0 zhD$=B;`a*2HhTh){&CX#zLEy3kRiQW!vZ}67rp}AW6Mong~Z}G&AF#FNZ-tsDInR!t_ zgJw8$%d1Z@rc)YEjb$HnIvT?_0{EB+=UmXzvVYu7>Id84E!U}KxHsyL{HB6vsqn1x zi!P_Lta*zFMer;7EY>c3C-*h5+P;}S-+$D8j0>F|TXghMyLP&xFC*Qhsn(yVSC1Zi zoNR+v>OovXYkV9$p3E}mq4M^?-M=KI(UqQ{<*Cws_$yH^RlEIIB)8{}K_m*v>CpT! zG2}>uCawlX0b_L&M`rFduOCqiHbe3}fnOEd-xXo+r(4Fv9}D#7ZZu)RX~KnA+1>G5 zSiDh!+8@#Zoxbp>+U9)PK4%X8q~Gy=?D9&k(0;E=JHksGeKxP?%w8L27DqvI0&EcxcS>9Jrxh<1 z>4=efHg!CN_^@z_hZ>i5qX?aTBV6>}e}vH&?lNW`6yrBo!WkhbEM;@OZ%>Uck*!oc zpwh=x^;*bf@~>Bi!u;9wc=68jPGRL%iBCQ;4ZQgzD+^1+KIm`v&>qsOL^@v@9V_>G zTOm@E7gBOBqNKjH>woOIKMf=K973q95v*5BFs?91&5xJsn){dO=vfTkAfAU8eV;|r zJ%Y&dacLq=TijXhH;` zHj;Z5GQ7#-LFWiB;dm=zRbH@|&@XX0ODD*vGRaEiIE-@>*$@C5_j3$qktc=OMG3jkj@Y7|)Q3a5LNN_sNN-&wa{!uGH_1_Rr6c z$}sT{8Q-F3Z`ALM{`r}!)}`Tkwb-e9g*p(k{c?w1VPf2E!WSBJM?9Cin?wuLBiMAG zjPr!oGte|U^bCx;Pn-UftPgn>Ma$NId=VjL1Hy90jCNp8tKLmYJh26PqYJ*?ySv&Y zj#eoD1h4a@7!Zpe&LA0~&uzkhyG~uyI8<>X6?+L6QnU9N8&hw{`kGITO*X%fXN&nS zSuzL2aErQ*RTqav+*>l6%GPiGzFsRde3#5k`))sdxXz3Hr`Xov7%r4ve5fxmOJP)K zF0q&z)9Wh${?vIN<}Fd1jN_peGyP#6wu#(Ein}sQISw~tJ_CQzUNW2KdDreJ^IYFV zxej$CIhE~_wg)Kd=lu1B_JT%EVH)Xy$X>+AI2ac0?ct~|-i0O$_Hdc;k%PV zEP&aTZYGkIf=O$OyW}+J-=gGt&ogF%Bh_)M(y_B^ccnw=4RWw9>8?!BKWjRkpF>wd z4fHTp6Ri#QekX34aVZkzuh^<*XyNRYRrjNSDQ}PC>XhI+>o##Fys|bf!)jCJCD?HF z*+Q7lBC7R;TZA+IA(+o)5yN~a!z#t4qL{uqBPxVT-`qWai(awbBcAg53+MZ#bKjEA zzo@Ii6)n9$@6_a#YEBcSYK> zmzsKI(Bo6zWr-)yD<`&=(OvU8)d+EYG0R|P-hq2pg+=5Uj+%7cB095?g_qTWHn-q& z5VLzszEwEQRZ~2)t%E*iPF0JUh{VR3p5@kkKG|&S`bx9$@!o+RZGeYpExNCgqiM4E z#34T2EcTVbp4C5PzI!+LhxHo``&XG;pO40#l3mO+OSmHQiHQ$b74-E*x6wov%xE?w zll!*(_sE7EZebo=x<|kCD(iAV{^?44%@yp~am(hI!s4B(1?73!loqVZ#+5e4(rd08 zn7er`a^^NyD0G2=BBTEd(1HP-Hc=60TL6%&v6hZ^gm1E&zk4KiP*@blgA>PgPj4_ z!bQE=rS;;&m5NB+bMMAd7iF^4FG(KHnf#3Rb9=LBM*nPx4i|zkS!MrJ%A~x7UfBps z=b{~bdK;{q*}eh-)ynSzT=ANv_LUT5`-#RZ_zxoV>akUHwz zXW1=YKSzU-{gdCNeJ!rzLLINs{b<&uvwa7la@6XCkv-z?ap?&avTCYTfd=nTZMd?RJ}Y>Qx-$Z0@FQkKX+~l*?&aMx zy+Wv0=z$U1hx&|oL3>`vcMUh8KDbN%0sQ&+4pe5BS?o_)uoO=1JE$Y^DV!so*xoB{ ztWiJNl_2O|vG5s2rgEA#$gbmm5IYGfUFBiiAPBLKd+iC*gAG%?+6Lia^s8^sbLOKc zp!3bD!4~D9&uitNmkr5}vbkrJ&Dkim&9UE+tvxNa_O#fVYq2%9sNnf-(a1N6c3nud zsXagP%{~3xob{u6xmLBCW3P4YdZfz+`9s1FUvB3Vrx9c}8wD>a zlTEib#`oG}>{oJ=Z^3IsSqo%2g{w9dTU6Zoh7s31sqBof!yy})Rn4p)^5_vi=0JuY z^W5-Lz5;#8JV9ddk}{qUwP$_fe`b9j$@;Dx>GpQCr;d9I>_iE*qp^-YuYndoGoY>c ztcK=l?X4OgFCofALcu!Ua3knq2tLU`{Wy0gGJI=B5=7ZFUb;#UH z9;7qih4`quKJVcvFL2QlR#K6OY%n^&Z6~sz?4Le5!zZTD$yiLM267SGIqldbX#H34 zIc1;hs6NZU4A+r7j&L+osH^@)oa4}2mQ&-y1>aZe7SHn*-~_aA(Bl}BgfjdHW#gH_ zzQ_8o5#-r`jqaGIz?_OUo?gf=xHo}@%h@1NBb5lXi+;ki@I_XT2>;-Yor^Yn^osQ) z;i#Rgxhsxz~-~$3_kc|Qcof*I2o!@_tB;&sD zQ=&R&9QjO*I4C_a%~8_EJTQAMk1RW%*1goT5=FD~L!v{x4;=9v&oQ2(tJ8DYD&{N_ z00%bpTsbGvV!~0pBBvhPF=qG5Y_NHX=eozA|Kwk~hEsw&*hoI?L>cPY?p&VmsVR?D z;HZ1eNI2SxX9nS7qB!Ij&5qRJ73Sl*RoY9^aic{z#B_{_^`+`G8)T{O$M9~c*cu1q zzNf@6)Tpt1@I{N364ZI1Xpv84C?s468Qv(h@{lE`;_}`Gjl)Dh!C=e~tQ6(({4>7t zntxj8c}k!Jya=~VkiO(r|MT9~m}^?a)K@q+Kk_Z7wkGmc2<%-gDYa-;70S2shG zYv7A_3Kw_{mqAY+?z4qpBvYd|{ z8!#|*I0L9-DMWqg(-%GapA<%Lz~IY_%CML#f3u9h){VXgEf^#V*mH0A#X5}E_fE9F zJsNv*X+oK13!8j(Gd_V8nUSu@X<6fn4E?k@Ce zaB~AVOFPcbYD`B-NWapGGmMTlN3nLRp2C?>xmB=BrsmydcW`0dirgm2)L%O|m3%G6 zQXx+`(7jT%RXyNbhjGrF?>Ns`hNGHiw3I8WV2|L2=b;XDNQ}wE+sj722#qDd!or2P zOpoiyzDTccNxjs~!vU1rVhikUv-62j_VwX{lOFay`gHe?hp|i;AZ^XPBrvl0T*QRQ;94ss)8<0_3=T#^DM8g;`_=SsyPFQE4(h$HK^oK z^e@9a`u7J2ZmZ8VK%522HP?%BbNftJS*rL-z1!fg#JPWizu@v>sMPsNFmsE9-XTept)GIN?T`ZZ2J;?F>q`+p8ZB+4dneH(ji; z+`b1(gfSLjKl1^x!5Pjl$8L2oU!2jzmq=(aqn>l$HGg}aR;Q}vTgONj#O+-40CYcL z+sZOpdf#ll($=bL&bIIV_TF=hn_S+S^|YKpeC9 zy>XH<(!utXSNw8b4hT&GDqg{560$+A&?NN&Z&r!}$Uq_8KuSO06Z_-HD;ON@RiKeP zmqPo4y+reQp1nVo2N^^&k@P2LVhU|sbiF>HI1}k9KS}*LAi_%8MWQ*OxRakoUOzwn zUK3*5hgV*!u!h~PYlM7Q9`}4jlAroB@3E%Gg`vA!?2rg(`(nDERm$&s@jxuD(Jd8Q z{8PpRTueFwv3M{2(ETO2jVLBxn~tFz6!DyKbO)ut7h_Cu!!k2~i9yFM6;$@VcG_Ru z`*Ygn?!Ff@*Yg-^gTEVyUOq$!Snb8s znmPUZKgw%#?0&Y_-74q9seJ~AB63Ptb@wOswvRH$4hd!p7`#JpkclJL zq?RcM9X_uQUmWr??-j%mLlznL^muBl?!5LjP-G34FhX@I2?twD2D8>2zV3%|&$gfF zd*$CPkMf<-QV`mA6yVfcbd2(vwMgN472NB=r~7v5YlRQJ!&~3w3VnO`L6Uv-Sqbl) zgSal-!@Z3M4jArb8Dr8oDor7tnilAkuGhVyvK1YrMT;Zv~8>+V*O*nASWmdVBZJiw zbvVAxe_)LkCdz$gUr%Fm%d4P-5-+rOtAbk#9nq86(N%6u%dClJpI49c!rcdov(Q}| zGWek`TG!jg`Hnqtsq_!M&ziZVoAfch*o*5g>f`LW1R&2q<1fh_PAwTuGa)T*d1i?t zopz`V;kC&`k8Xh1F<5{$F{q6#=Ll}MM(FsufbYU_nT!iM&C1+$su>REd|Hp0=I{FV29i-bLGRhK)pd|y zKf~w3p>^DYf0jLVzhIK*nM*O;jUE6q`3yUB&q&wE{a)#8$ubg#3cI5l40~h^ZG*%` zo}f80xkY#Me!4J9tov5e3Q!5Md6O$zZl^4I$agfm?L+TtWW9?F-F+{UFIDtQ#cT*p z|CL+ydRLURe98cAF*hyoI_G+Cn$;?J0mRHDUrSW~w%|fI7I!V$9Oja=bgm*@YBXP?0cq#W)VZ@*EtW-)v2&`G;`sSN|5a}&lncp`s{U8~bSOD8q_KrW zwFtgd@U2!#N$1*RsyNarC+!p5O4=VAQ?HYSWD3FwFD|N;O1V)jHj2WbB$gK&)oQs? zY$Ven6X-UNm5YN=stOmXCADpgmj^2Y!jn4<^-vN4$IIm)s#H`8K0B}Laj{Vm#>oT8 z1JzJS+RrqPjo6L$Gh-u_dbL_Arav{2&#yFma~GI58V&KO(HI-9I8q{SxmX%1lpB>A z?5ql7?ise5gsq)y(TU+UG+Y_1ifXE+a-=et97&Eg*A|;=lZPsT=7``aS4zS|eKrO| zNkfe_Dk4~?$nUs{DuObq6E)yk=t?ZMij$%x2P4r!5vwv7iWR}4)4ERUIu+G~0t72D zPN^|0xMxIe`!PDqBr~eqXpE1kqon<4RUivq@k8RZuB@VSdj%m#+8^}kq>;HsQU)wWpPlzs0+!9#QnH9Cy=YHlFr3hO)yTFV50g^C~0CI+dP`g)GC8R$$GFC z(VHtoSI7bbrhLK-4=U}|{_=>J;{;<`qCN`A^bqoJr{?S^lVq$oAo(AIQ_mNYVftIk z3@hqMoteEmReoyP{wy7Akxb}uGNU2YTKa{4n>W-dWr5#F+AmRSMv@>11_o8sL?#{d ze^I5BYTVvKJrt8;yZry~XrXj|7= zHYP^U5={#MD`9{MKUVj?lWlr5uQIH0Qv?LRMv=@g9~+gHl!D|4`Pr?1m3ne}vOQ^k z)A|$~x7Ss9x1o{jmMj#pSS}e-X=BN7RjOq-SV={U0rWX=x&?n%t5YZ@%~H<5kT{Wi>6CeR!}D18DpAP+O8y&Mw6+LN^Q*Nh_ovl92(L*mM-9jQXr2( zLgT|UeE8c^bA_6Wv^1TK4?qZ5Q^j*DO%ZQ5GO`=IMMIs=2$ThtVSkdezoA~8yf)or zoI=u>X;hq2s^9)rGBw;9kQvs*dOqoVs;-Mh(s{1g`Lx7DH}5aL4x_A}h*{D(%q1l3 zyjv$y6)ICf5^&h~fQ6F~iY7_>+qxrx8JBZ_c9Be|NzyhVi<;u;*cckGCXBx;(;@}d z5=F!4{DLG#W_Er-?Vw$(k*t@kL!$JrE~a!I^b3i4`|k=x`9PupGf3YyIB!j zeg}H#pTsr2nesoJ@(WF0ts8=M?L+TUlt4?%l$Q0;NY*j-ZhXUPSuw^={H-vU`NplVV%FSiv2J+FD3)4JMsmsnfz>lvAdT5^wPIh*-Z4LDw@pX1O?z!E02{c8Uiy zeT9`|T*jp_Iy8!0K5Gu5TFI-GRLZ55!F=SLiY9mGH|6>yopV)4$up(Rk+s{U!DRh_ zL}RSR+&uQ^m>e1u;dldYNmB-IT-^Sqrdu(Y7BupiHR*335?oTLTo_N1-Dx|iQz^pT za_5keXQikToFaT|7@FK3>)lSh+nWL&rS0KHvR@b>`p0E-%sp4sqA7M>)mxRJ`I`Kz5uEe% zhdb_zl;i?dMbpO?$|82gHQDqmtBG#Y+-V;BP;%_UnvAgnjGug8@lw^9eN}%5M}@2W zC(SPzxMEfQLZ@v0f^KO3;m#kY?3Cpr;6&Jc1u7$gNEai*cwEr~RzuyP5UZ)hm9o)9 zJzi8T14It9rdJ2M*k~ljetlG$8=uP`s<~(*a;rB}A}&}23(Z|}?;DjNYNz@Igp&RC zK%rSkj(to5RBcG|MeF@FoBGRjHV=n~TVe+>q?mE+gpNZJH#=2SgHY`stqf|At^g$a zMW(96UBb7rvN)Me_E(#`=t`oe`JO_Z@hH8KOj|J6*iv~TZRTjAiq=ZVM8f!uw1=KU zGCdlkz$L`Te!Y3@w@1~&oOpw*XrAeGG9^E8VY9PRXb}(xJD;yG(eEif5!JdCME!yy zQn5?~Ai5@iFtUn060PT?BBc~}6^qghGEhnT#iZSKQ2P6<6*>0j79lgUUKGi2c@8HXd4yFt?HzepC-96O9FvM4RD`N3 z=ORFDX@Yh;j;yxCR`~DYs3xP#@XE@$l4F#0+PAEP>QAPG@FtP}I9Q5z8+Ec8oe4Fc zUl7@Jrg|EP`?U4@SuZOqs%8Cm4_#8{sGiD#InY*n*v0l+*5Qcx=8;2*be-_^!Nn&D zm6!%3x>#L__?*pn8WwjO z#)crmMKqE(G(izcITNMyzPKnKO)XE#JF~vTL{Gab;QfJPbZBo6*tL%h){WN`)VQS!~`d=}9kOt!|$X z9jqc34_nZ!RyxJ#J!puvt$f_dCn-BeM9JpelAI%u)mZodk*a=@4&D!18xcAp2{Mf& zcUtD=A<0og1{>E#V?1VBdc6tROtM{FtBT#5v5ImuJ!;0(4M<=g^F|WQr<2NP2^Wnq z^@CN!WOOWE3N}iZuaO~H8zNkU9f(KyUiKf*jGypSA!AH`0M9fFFrMZQX{r}k5fU*ZA zMdf3%e415_l4gM16Qjw-ZH{Hf^6OXHQ1Jk}vc|kHj z`XP^6C@4ZQAH*8tQmTd?Ev$?}O6dSH8dm6dVI^tqlFOIa%t+e5FL5yuqCc4Ods0!v z%1j)&XA-psWG0mw!RA^}D#D2*I34m`713zcOIF~iJRS|LA5{@kfI?P}CxmhSh$}@z zk^$AUY-hDvYn4R|Zjp+l1IJGjLsv=rDP)CQk{;+v56!qz30?dcIjgD#J*ptLT6WWY zeIl)OLq#72x6=2)8K^LVafL=QxE3v)B|%ErX`&ZlD-x(F~H6CC-BWo zCOaRtnfGCLjFfVLXNrxKYy^Ed1A_OHYC+(KUqSfjLj6e*t)wTyNLiLOU>q z?mc7X2n?UGhi>(br*~r$$r_O@%`{gfgrD>gI-kUkAy$w~%m1^|T$|4C83>)<*AwXp zMFAywEr16Eg)qEe{II^fGFe@;V% zcW5(R<6=k%nu>{gf4cpBQ^6UYr6LFEH!|b!MwKlJKklPQa7)d*5&F&p^9a|I&Tq$o zbe(K>(YJ08J)}a0x>PO6sg~Vy(FQeV?u%|lpEK#|e9F}7r~Tr2=+pVUmAhmylFsK) zB#PXFXnJgdw3f&qJ0Q9Hf~D(ViE;oKbX+kRbd$R6R^>}p#iAyGO}v(LzG6`97OIx! zmSA~C+Qg${a!76st*>V%4VEchg#Iijzckt+G#PMiHjtdt&?)Le3Oea5F{;0iGDg0U#?t{be0IO&Jyv^ zSD6{=Eb-bnWQwdhOLSB?(OI$t^JX=Z^}RJsdoy^+l zWx;u^d)H7=jIU1qax5KAfs6NQgE?+ET;PcC$O~v-TZl%*;gM0)_?R12gHUy4YCB8P zXrr?PGiWT8vt#hmv!}kqm_||DMdjN$%oi^ted&k`DoURyO#)a3g3i#7^g(XPjxz8g z5;D4%oQ!d=*u23^0Bt0)qPpB)s5WNQ)ykHH*W{ANa^(^GRN3mfGZvLAXRc5?%0PH8 zinGfiIml2|HYe>n<;d84OVIUpIGdl=R#H8ifAL9|Q(DG^OD|ga(yGX-lqMM{kw|__ zuSR;VWV^h3DHh4ZTFK;md>B>2w2@F!rh1Fwc({u3F(l_z$j=oU`L-vS!bm9%Iq96# ziF(zI^FuRZqXSTUeIGN^+pzR?6$|4vR+J?hNPQtpV^Ns2_BRB@4H6ZpI&l-#88zo# zS#6v_J(G9K8in*ca%ML@>+R25MJ+qXL`s}U#iZnzA70l&*qj(Snn3$DmRE=g)t7~8 ztOp^vhA)uMNkFDUz^@UIl4>bJWujF|u}H0ODFk$3iunfjkiV`p5x4nKI5{fUS8IId z>(62ov_7fMJX^Ta&Uec^(?dl$E$RLfwxX)pA*e`7S~+FO1+yY#zEn%klg|5vpr=2L zN>m4j-&7S`5rJSMd|VI{6UY|JA82T4K)#DV`hr#X5?*EJ2q5EgceLhVF~XF4B;R)W zN{+q_A1pTiZucG<#d>+c%5MMx0#uf=JU_@l*iySV#6m7)t^Uq?cHesLnin2zKdo<& zhJkhnPlgZ%iz;+|%Z?3~g#pDfx?H~K|2+D`&ph#iOaAER|L4RXUsL|T%;dAby7%C( zegCT~zW3qQ|E~Z3{Xg|L@B5jbfBc$t-+$-lw!HA~fBLt6`rH3?^rNqzcmGrKe)gre z?Wq0n@$X#qnOi^c8!tX_aPLKxM_>5BTdrC4y-yu#Bn$7{crH7w*Y52>3{V)IIGoSh7+V#W#anb*1z5CM_FVoko-W=W%uGLEkZ_FX5y7R!jllQIPbM@5|SMI&? zs>{}0v1W4h+I8*63OjfG>??!hy6K5`yrw{u>G4n-!d&(bhAD4wI2PjmJ>#V#(TNSmDjRYN##=k`}AzD}{af@~oh^85)P)r@GsCt{sW=vyS4kDwG!18KT-*K!NnSBV;EQPmy(C zMOPcil&$27L$Od#HtV3hdJQcNijPl<=1K!_*3f=88a3DIgvicLXxLnH6I8lUZmMQ!E+=;Q(J zR4Le>sI5V2An3L1O$P!-P;nfpCUTf`4HQJiFkC*PZVmO^lk;3~+=lSe#d!AkY4vC= z`yCq!t8IC--D%8IP1pxjMG6x z*PtL#gpk^4)b7+(J7r-tJ0BVnRkaL*6cvX2P)jx=Oh%+CXNrY1J2+FYJ6Wi4re%)F zLY=@vVj-M48!s!ETM)%=K~=XJ8d8Zi?RrBT5ld~%)nDQ(lN8uiX%)3e&-U^*R}AP@ zh5Hf=o~|a8sMr!jv&FXMihM4D7&6Mrnb~pJo-0Giv*{rP4yM{hweAP6+{WlxbFKBf z);2jSyx(6Y3-wuAZL!g|Q0WG{`x19&bgE#FGd`$NHSLw3$@t(tPrlH8R$2(O93tlgxe{0Qw$r3DivE*1}Tk3YRZ7x z83MhgwZ7_d$FOK4ZI`rTaY)^3mx?IOB7;ay{%2A%sKx%b+ez%|#CqB77=PTzyIO*$ zzQo`5mq)E36G?`QT$A6RC%Yki1QhK@fKOu&>EP2&@f@6g#D=T^y`5>ZTb!OO@QNo& zhmx1^gE3ZupvvoN-+PczSA*uJ zhCR}#>GmUf^HY}+KOYQ&@>-WCwbWHZj52Di77Ar;HZlr!&tVv&ETLGYLLN{nXh9{& ztq7VGl6&n3LTPUkA*BJgNn38yo^#=yhTUoKZFQuW10;buF)EirP^;A_OeMGMN=)>- z>x-`8dct_kBH1}nNc*(61Ctd|IX2W%YcbN;ww08zd^3+0P?Xw{c2$O3joRM?qT8jn zO+wadKd&ywX|B8LwU{Lqv_GzE7gxaB-4N2rZffrKn!8ogh2mLBprnEAHP2fAx}jXk z*Ca*Ce2d8G$pGstTJtbqv@2JOUu>QVFcO>N}OwDPV!(r7IXK=2luPb?Rl0mzHekNC<77uDhXl+f}zs=lm z7g3-Jp>|t+N|W5^UAHwCHK@9Vgq<7Vt|lGx6ZUCCVYNC6YveO(V>fL42*P+b<7llr zRgNqo`)-BQU9_6-cSfBYjw+PZyAd^XCCZi6d)dl|)upyCu%n~>vi_^FMt!)VjYoIO zhxEQ4w#RZkPXMWJPigz3w)OJ3l}}Q3j_3huUHGunl+JjY%e=6`y0g`O+IErbB8x?{ zUEUwBbV%Z*kg>TiS)fSPl6BUff)t*&pG0oij;Uzf)K;T35cDEMCHSiraz$80G{2;% zC&iLE6A)H{Qoy@V1V6(X!-7CQtDyyS@wDqri>l|Km6{)CWg$cY9y9YjJd z2mEqCvyz2;`7`ZLG_-+N6q3GUsjJw~aM_iahVG>r$dQ-n7DgJ?2JpiGwA46$uwmqP zd_r^9dd0S>b6YbMC$owFl=1H=nkgo*31+Ag>xh;6jD`Db+!y4MN}gVTI)=vGET{{+ zyWJK01~{sh&$7Qsv8K*y>_?ICZRcESxMpspGM;2waX(fSB7SXcZekOs|5ooR0$BRMzx_+ zB$tb)?R}7?Gd#Rn$vU;S^m=)(2^iWrQ|i!h~6M$hOSpiZ~)}pECkJ8jN)aMfl>&epB^n-hNEE={}46`>sl?8%2Iwdq0SKEYPAo6lMKDU~P=I(Xj7T@|NpgX)GxXSG#YVUL$l z>P0phjA|^KTi$}X70<@((OAQf>dKO1sQoZ%y}w!rRpm58IA5vzpbXw^2Pf{o>A-!P z-@SM8ev)`Q_a8j)u0zV`hl*0#AuK3_dGDIsbLha{dnXUCJhXq}puQcosSvK&(Av1T zH8pep^yJLseTOXp*g9}e>+t@`)@_q}c3Az^{Ra;mKCt(|bZhVQlxnpm?%UToGH!6Z=9~TL@R|+&^`wH8Xj5|ABq2 zcTG)CxAsi74o=P-c<1E4RjplzCR-Ex_D$|<9k_3@HFckI*%SNpTI_{|Fm}3zAqZdp zsTIP&R;9oVKHXfbr6g-ARPLWVc=+h7QA7BPlW*O#{@?81c;FX*_sav%-ShIcul&|G zFL=WZfAF^IuNB8Xci`dQx$A<(KfL&R7yt8rvFN=g|J&d_m+m@#WXl)-?O&bvv#GCM z^BX7DPkrtu@A-G1`@z%0*I#wX<;VZ<(I*cb`{+jwec*?``}~&|{?g>4`@i+ue>MEa z@B7e+zgzw@-@Eg7x0IK@_!n3HhpPtu;K}d4{f8HSXWhEL{2r?={xkU3I&a0R-s8z! zJ-v&(@xZ}N)6+LjOzA6;WbY>@o$_k*e=kxWENA>9`zaXGT5W0fBFSK0Kyi!^a*74sOy+kh>gr~x_y1!Acp7T-p_$id2Zzl4GsTQl`^V@tF zn-4zwb6!}FFVOdXQeyU3DC0*Q_>cl0FyPZje8z>3tosKlz>8PowJ5c7QQ6OCq549LkaqhX!^6wV9tN4 zf)S& + + + Debug + AnyCPU + {EA1C601E-D853-41F7-B9EB-276CBF7D1FA5} + {786C830F-07A1-408B-BD7F-6EE04809D6DB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + Library + LiveQuery.PCL + LiveQuery.PCL + v4.5 + Profile111 + 0.1.0 + + + true + full + false + bin\Debug + DEBUG; + prompt + 4 + + + true + bin\Release + prompt + 4 + + + + + Source\AVLiveQuery.cs + + + Source\AVLiveQueryEventArgs.cs + + + Source\AVLiveQueryExtensions.cs + + + + + {92B2B40E-A3CD-4672-AC84-2E894E1A6CE5} + RTM.PCL + + + {659D19F0-9A40-42C0-886C-555E64F16848} + Storage.PCL + + + + \ No newline at end of file diff --git a/LiveQuery/LiveQuery.PCL/Properties/AssemblyInfo.cs b/LiveQuery/LiveQuery.PCL/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..5cf7adb --- /dev/null +++ b/LiveQuery/LiveQuery.PCL/Properties/AssemblyInfo.cs @@ -0,0 +1,26 @@ +using System.Reflection; +using System.Runtime.CompilerServices; + +// Information about this assembly is defined by the following attributes. +// Change them to the values specific to your project. + +[assembly: AssemblyTitle("LiveQuery.PCL")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("")] +[assembly: AssemblyCopyright("${AuthorCopyright}")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}". +// The form "{Major}.{Minor}.*" will automatically update the build and revision, +// and "{Major}.{Minor}.{Build}.*" will update just the revision. + +[assembly: AssemblyVersion("1.0.*")] + +// The following attributes are used to specify the signing key for the assembly, +// if desired. See the Mono documentation for more information about signing. + +//[assembly: AssemblyDelaySign(false)] +//[assembly: AssemblyKeyFile("")] diff --git a/LiveQuery/LiveQuery.Test/LiveQuery.Test.csproj b/LiveQuery/LiveQuery.Test/LiveQuery.Test.csproj new file mode 100644 index 0000000..c382c64 --- /dev/null +++ b/LiveQuery/LiveQuery.Test/LiveQuery.Test.csproj @@ -0,0 +1,41 @@ + + + + Debug + AnyCPU + {F907012C-74DF-4575-AFE6-E8DAACC26D24} + Library + LiveQuery.Test + LiveQuery.Test + v4.7 + 0.1.0 + + + true + full + false + bin\Debug + DEBUG; + prompt + 4 + + + true + bin\Release + prompt + 4 + + + + + ..\..\packages\NUnit.2.6.4\lib\nunit.framework.dll + + + + + + + + + + \ No newline at end of file diff --git a/LiveQuery/LiveQuery.Test/Test.cs b/LiveQuery/LiveQuery.Test/Test.cs new file mode 100644 index 0000000..9bd89d3 --- /dev/null +++ b/LiveQuery/LiveQuery.Test/Test.cs @@ -0,0 +1,10 @@ +using NUnit.Framework; +using System; +namespace LiveQuery.Test { + [TestFixture()] + public class Test { + [Test()] + public void TestCase() { + } + } +} diff --git a/LiveQuery/LiveQuery.Test/packages.config b/LiveQuery/LiveQuery.Test/packages.config new file mode 100644 index 0000000..bbb222f --- /dev/null +++ b/LiveQuery/LiveQuery.Test/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/LiveQuery/LiveQuery.Unity/LiveQuery.Unity.csproj b/LiveQuery/LiveQuery.Unity/LiveQuery.Unity.csproj new file mode 100644 index 0000000..2e35ef5 --- /dev/null +++ b/LiveQuery/LiveQuery.Unity/LiveQuery.Unity.csproj @@ -0,0 +1,56 @@ + + + + Debug + AnyCPU + {3251B4D8-D11A-4D90-8626-27FEE266B066} + Library + LiveQuery.Unity + LiveQuery.Unity + v4.7 + 0.1.0 + + + true + full + false + bin\Debug + DEBUG; + prompt + 4 + false + + + true + bin\Release + prompt + 4 + false + + + + + + + + Source\AVLiveQuery.cs + + + Source\AVLiveQueryEventArgs.cs + + + Source\AVLiveQueryExtensions.cs + + + + + {1E608FCD-9039-4FF7-8EE7-BA8B00E15D1C} + RTM.Unity + + + {A0D50BCB-E50E-4AAE-8E7D-24BF5AE33DAC} + Storage.Unity + + + + \ No newline at end of file diff --git a/LiveQuery/LiveQuery.Unity/Properties/AssemblyInfo.cs b/LiveQuery/LiveQuery.Unity/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..e93ac80 --- /dev/null +++ b/LiveQuery/LiveQuery.Unity/Properties/AssemblyInfo.cs @@ -0,0 +1,26 @@ +using System.Reflection; +using System.Runtime.CompilerServices; + +// Information about this assembly is defined by the following attributes. +// Change them to the values specific to your project. + +[assembly: AssemblyTitle("LiveQuery.Unity")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("")] +[assembly: AssemblyCopyright("${AuthorCopyright}")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}". +// The form "{Major}.{Minor}.*" will automatically update the build and revision, +// and "{Major}.{Minor}.{Build}.*" will update just the revision. + +[assembly: AssemblyVersion("1.0.*")] + +// The following attributes are used to specify the signing key for the assembly, +// if desired. See the Mono documentation for more information about signing. + +//[assembly: AssemblyDelaySign(false)] +//[assembly: AssemblyKeyFile("")] diff --git a/LiveQuery/Source/AVLiveQuery.cs b/LiveQuery/Source/AVLiveQuery.cs new file mode 100644 index 0000000..f680ec4 --- /dev/null +++ b/LiveQuery/Source/AVLiveQuery.cs @@ -0,0 +1,225 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using System.Collections; +using System.Collections.Generic; +using LeanCloud.Storage.Internal; +using LeanCloud.Realtime; +using LeanCloud.Realtime.Internal; +using System.Linq; +using System.Linq.Expressions; + +namespace LeanCloud.LiveQuery +{ + ///

+ /// AVLiveQuery 类 + /// + public static class AVLiveQuery + { + /// + /// LiveQuery 传输数据的 AVRealtime 实例 + /// + public static AVRealtime Channel { + get; set; + } + + internal static long ClientTs { + get; set; + } + + internal static bool Inited { + get; set; + } + + internal static string InstallationId { + get; set; + } + } + + /// + /// AVLiveQuery 对象 + /// + /// + public class AVLiveQuery where T : AVObject + { + internal static Dictionary>> liveQueryDict = new Dictionary>>(); + + + /// + /// 当前 AVLiveQuery 对象的 Id + /// + public string Id { get; set; } + + /// + /// 根据 AVQuery 创建 AVLiveQuery 对象 + /// + /// + public AVLiveQuery(AVQuery query) { + this.Query = query; + } + /// + /// AVLiveQuery 对应的 AVQuery 对象 + /// + public AVQuery Query { get; set; } + + /// + /// 数据推送的触发的事件通知 + /// + public event EventHandler> OnLiveQueryReceived; + + /// + /// 推送抵达时触发事件通知 + /// + /// 产生这条推送的原因。 + /// + /// create:符合查询条件的对象创建; + /// update:符合查询条件的对象属性修改。 + /// enter:对象修改事件,从不符合查询条件变成符合。 + /// leave:对象修改时间,从符合查询条件变成不符合。 + /// delete:对象删除 + /// login:只对 _User 对象有效,表示用户登录。 + /// + /// + /// + public void On(string scope, Action onRecevived) + { + this.OnLiveQueryReceived += (sender, e) => + { + if (e.Scope == scope) + { + onRecevived.Invoke(e.Payload); + } + }; + } + + /// + /// 订阅操作 + /// + /// + /// + public async Task> SubscribeAsync(CancellationToken cancellationToken = default(CancellationToken)) { + if (Query == null) { + throw new Exception("Query can not be null when subcribe."); + } + if (!AVLiveQuery.Inited) { + await Login(); + AVLiveQuery.Channel.OnReconnected += OnChannelReconnected; + AVLiveQuery.Channel.NoticeReceived += OnChannelNoticeReceived; + AVLiveQuery.Inited = true; + } + await InternalSubscribe(); + var liveQueryRef = new WeakReference>(this); + liveQueryDict.Add(Id, liveQueryRef); + return this; + } + + static async void OnChannelReconnected(object sender, AVIMReconnectedEventArgs e) { + await Login(); + lock (liveQueryDict) { + foreach (var kv in liveQueryDict) { + if (kv.Value.TryGetTarget(out var liveQuery)) { + liveQuery.InternalSubscribe().ContinueWith(_ => { }); + } + } + } + } + + static async Task Login() { + var installation = await AVPlugins.Instance.InstallationIdController.GetAsync(); + AVLiveQuery.InstallationId = installation.ToString(); + AVLiveQuery.Channel.ToggleNotification(true); + await AVLiveQuery.Channel.OpenAsync(); + AVLiveQuery.ClientTs = (long) DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1)).TotalSeconds; + var liveQueryLogInCmd = new AVIMCommand().Command("login") + .Argument("installationId", AVLiveQuery.InstallationId) + .Argument("clientTs", AVLiveQuery.ClientTs) + .Argument("service", 1).AppId(AVClient.CurrentConfiguration.ApplicationId); + // open the session for LiveQuery. + try { + await AVLiveQuery.Channel.AVIMCommandRunner.RunCommandAsync(liveQueryLogInCmd); + } catch (Exception e) { + AVRealtime.PrintLog(e.Message); + } + } + + static void OnChannelNoticeReceived(object sender, AVIMNotice e) { + if (e.CommandName == "data") { + var ids = AVDecoder.Instance.DecodeList(e.RawData["ids"]); + if (e.RawData["msg"] is IEnumerable msg) { + var receivedPayloads = from item in msg + select item as Dictionary; + if (receivedPayloads != null) { + foreach (var payload in receivedPayloads) { + var liveQueryId = payload["query_id"] as string; + if (liveQueryDict.TryGetValue(liveQueryId, out var liveQueryRef) && + liveQueryRef.TryGetTarget(out var liveQuery)) { + var scope = payload["op"] as string; + var objectPayload = payload["object"] as Dictionary; + string[] keys = null; + if (payload.TryGetValue("updatedKeys", out object updatedKeys)) { + // enter, leave, update + keys = (updatedKeys as List).Select(x => x.ToString()).ToArray(); + } + liveQuery.Emit(scope, objectPayload, keys); + } + } + } + } + } + } + + async Task InternalSubscribe() { + var queryMap = new Dictionary { + { "where", Query.Condition}, + { "className", Query.GetClassName()} + }; + + Dictionary data = new Dictionary { + { "query", queryMap }, + { "id", AVLiveQuery.InstallationId }, + { "clientTimestamp", AVLiveQuery.ClientTs } + }; + string sessionToken = AVUser.CurrentUser != null ? AVUser.CurrentUser.SessionToken : string.Empty; + if (!string.IsNullOrEmpty(sessionToken)) { + data.Add("sessionToken", sessionToken); + } + var command = new AVCommand("LiveQuery/subscribe", + "POST", + sessionToken, + data: data); + var res = await AVPlugins.Instance.CommandRunner.RunCommandAsync(command); + Id = res.Item2["query_id"] as string; + } + + /// + /// 取消对当前 LiveQuery 对象的订阅 + /// + /// + public async Task UnsubscribeAsync() { + Dictionary strs = new Dictionary { + { "id", AVLiveQuery.InstallationId }, + { "query_id", Id }, + }; + string sessionToken = AVUser.CurrentUser != null ? AVUser.CurrentUser.SessionToken : string.Empty; + var command = new AVCommand("LiveQuery/unsubscribe", + "POST", + sessionToken, + data: strs); + await AVPlugins.Instance.CommandRunner.RunCommandAsync(command); + lock (liveQueryDict) { + liveQueryDict.Remove(Id); + } + } + + void Emit(string scope, IDictionary payloadMap, string[] keys) { + var objectState = AVObjectCoder.Instance.Decode(payloadMap, AVDecoder.Instance); + var payloadObject = AVObject.FromState(objectState, Query.GetClassName()); + var args = new AVLiveQueryEventArgs { + Scope = scope, + Keys = keys, + Payload = payloadObject + }; + OnLiveQueryReceived?.Invoke(this, args); + } + } +} diff --git a/LiveQuery/Source/AVLiveQueryEventArgs.cs b/LiveQuery/Source/AVLiveQueryEventArgs.cs new file mode 100644 index 0000000..98e860f --- /dev/null +++ b/LiveQuery/Source/AVLiveQueryEventArgs.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using LeanCloud; +using System.Collections; + +namespace LeanCloud.LiveQuery +{ + /// + /// AVLiveQuery 回调参数 + /// + public class AVLiveQueryEventArgs : EventArgs + where T : AVObject + { + internal AVLiveQueryEventArgs() + { + + } + + /// + /// 更新事件 + /// + public string Scope { get; set; } + + /// + /// 更新字段 + /// + public IEnumerable Keys { get; set; } + + /// + /// 更新数据 + /// + public T Payload { get; set; } + } +} diff --git a/LiveQuery/Source/AVLiveQueryExtensions.cs b/LiveQuery/Source/AVLiveQueryExtensions.cs new file mode 100644 index 0000000..a15218c --- /dev/null +++ b/LiveQuery/Source/AVLiveQueryExtensions.cs @@ -0,0 +1,25 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace LeanCloud.LiveQuery +{ + /// + /// AVLiveQuery 扩展类 + /// + public static class AVLiveQueryExtensions + { + /// + /// AVQuery 订阅 AVLiveQuery 的扩展方法 + /// + /// AVLiveQuery 对象 + /// Query. + /// Cancellation token. + /// The 1st type parameter. + public static async Task> SubscribeAsync(this AVQuery query, CancellationToken cancellationToken = default(CancellationToken)) where T : AVObject { + var liveQuery = new AVLiveQuery(query); + liveQuery = await liveQuery.SubscribeAsync(); + return liveQuery; + } + } +} diff --git a/RTM/RTM.PCL/Properties/AssemblyInfo.cs b/RTM/RTM.PCL/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..669b160 --- /dev/null +++ b/RTM/RTM.PCL/Properties/AssemblyInfo.cs @@ -0,0 +1,26 @@ +using System.Reflection; +using System.Runtime.CompilerServices; + +// Information about this assembly is defined by the following attributes. +// Change them to the values specific to your project. + +[assembly: AssemblyTitle("RTM.PCL")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("")] +[assembly: AssemblyCopyright("${AuthorCopyright}")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}". +// The form "{Major}.{Minor}.*" will automatically update the build and revision, +// and "{Major}.{Minor}.{Build}.*" will update just the revision. + +[assembly: AssemblyVersion("1.0.*")] + +// The following attributes are used to specify the signing key for the assembly, +// if desired. See the Mono documentation for more information about signing. + +//[assembly: AssemblyDelaySign(false)] +//[assembly: AssemblyKeyFile("")] diff --git a/RTM/RTM.PCL/RTM.PCL.csproj b/RTM/RTM.PCL/RTM.PCL.csproj new file mode 100644 index 0000000..72b7e89 --- /dev/null +++ b/RTM/RTM.PCL/RTM.PCL.csproj @@ -0,0 +1,216 @@ + + + + Debug + AnyCPU + {92B2B40E-A3CD-4672-AC84-2E894E1A6CE5} + {786C830F-07A1-408B-BD7F-6EE04809D6DB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + Library + RTM.PCL + RTM.PCL + v4.5 + Profile111 + 0.1.0 + + + true + full + false + bin\Debug + DEBUG; + prompt + 4 + + + true + bin\Release + prompt + 4 + + + + + Internal\AVIMCorePlugins.cs + + + Internal\IAVIMPlatformHooks.cs + + + Internal\Command\AVIMCommand.cs + + + Internal\Command\AVIMCommandRunner.cs + + + Internal\Command\AckCommand.cs + + + Internal\Command\ConversationCommand.cs + + + Internal\Command\IAVIMCommandRunner.cs + + + Internal\Command\MessageCommand.cs + + + Internal\Command\PatchCommand.cs + + + Internal\Command\ReadCommand.cs + + + Internal\Command\SessionCommand.cs + + + Internal\DataEngine\Controller\DateTimeEngine.cs + + + Internal\DataEngine\Controller\DictionaryEngine.cs + + + Internal\DataEngine\Controller\StringEngine.cs + + + Internal\Message\Subclassing\FreeStyleMessageClassInfo.cs + + + Internal\Message\Subclassing\FreeStyleMessageClassingController.cs + + + Internal\Message\Subclassing\IFreeStyleMessageClassingController.cs + + + Internal\Protocol\AVIMProtocol.cs + + + Internal\Router\AVRouterController.cs + + + Internal\Router\IAVRouterController.cs + + + Internal\Router\State\RouterState.cs + + + Internal\Timer\IAVTimer.cs + + + Internal\Timer\Portable\AVTimer.Portable.cs + + + Internal\WebSocket\IWebSocketClient.cs + + + Internal\WebSocket\Portable\DefaultWebSocketClient.Portable.cs + + + Public\AVIMAudioMessage.cs + + + Public\AVIMBinaryMessage.cs + + + Public\AVIMClient.cs + + + Public\AVIMConversation.cs + + + Public\AVIMConversationQuery.cs + + + Public\AVIMEnumerator.cs + + + Public\AVIMEventArgs.cs + + + Public\AVIMException.cs + + + Public\AVIMImageMessage.cs + + + Public\AVIMMessage.cs + + + Public\AVIMMessageClassNameAttribute.cs + + + Public\AVIMMessageFieldNameAttribute.cs + + + Public\AVIMMessageListener.cs + + + Public\AVIMNotice.cs + + + Public\AVIMRecalledMessage.cs + + + Public\AVIMSignature.cs + + + Public\AVIMTemporaryConversation.cs + + + Public\AVIMTextMessage.cs + + + Public\AVIMTypedMessage.cs + + + Public\AVIMTypedMessageTypeIntAttribute.cs + + + Public\AVRealtime.cs + + + Public\IAVIMListener.cs + + + Public\IAVIMMessage.cs + + + Public\ICacheEngine.cs + + + Public\ISignatureFactory.cs + + + Public\Listener\AVIMConversationListener.cs + + + Public\Listener\ConversationUnreadListener.cs + + + Public\Listener\GoAwayListener.cs + + + Public\Listener\MessagePatchListener.cs + + + Public\Listener\OfflineMessageListener.cs + + + Public\Listener\SessionListener.cs + + + + + + + + ..\..\packages\Websockets.Pcl.1.1.9\lib\portable-net45+win+wpa81+wp80+MonoAndroid10+xamarinios10\WebSockets.PCL.dll + + + + + {659D19F0-9A40-42C0-886C-555E64F16848} + Storage.PCL + + + + \ No newline at end of file diff --git a/RTM/RTM.PCL/packages.config b/RTM/RTM.PCL/packages.config new file mode 100644 index 0000000..105fd5f --- /dev/null +++ b/RTM/RTM.PCL/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/RTM/RTM.Test/RTM.Test.csproj b/RTM/RTM.Test/RTM.Test.csproj new file mode 100644 index 0000000..66eada9 --- /dev/null +++ b/RTM/RTM.Test/RTM.Test.csproj @@ -0,0 +1,41 @@ + + + + Debug + AnyCPU + {A1BBD0B5-41C6-4579-B9A3-5EF778BE7F95} + Library + RTM.Test + RTM.Test + v4.7 + 0.1.0 + + + true + full + false + bin\Debug + DEBUG; + prompt + 4 + + + true + bin\Release + prompt + 4 + + + + + ..\..\packages\NUnit.2.6.4\lib\nunit.framework.dll + + + + + + + + + + \ No newline at end of file diff --git a/RTM/RTM.Test/Test.cs b/RTM/RTM.Test/Test.cs new file mode 100644 index 0000000..4f8a269 --- /dev/null +++ b/RTM/RTM.Test/Test.cs @@ -0,0 +1,10 @@ +using NUnit.Framework; +using System; +namespace RTM.Test { + [TestFixture()] + public class Test { + [Test()] + public void TestCase() { + } + } +} diff --git a/RTM/RTM.Test/packages.config b/RTM/RTM.Test/packages.config new file mode 100644 index 0000000..bbb222f --- /dev/null +++ b/RTM/RTM.Test/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/RTM/RTM.Unity/Properties/AssemblyInfo.cs b/RTM/RTM.Unity/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..5485e9a --- /dev/null +++ b/RTM/RTM.Unity/Properties/AssemblyInfo.cs @@ -0,0 +1,26 @@ +using System.Reflection; +using System.Runtime.CompilerServices; + +// Information about this assembly is defined by the following attributes. +// Change them to the values specific to your project. + +[assembly: AssemblyTitle("RTM.Unity")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("")] +[assembly: AssemblyCopyright("${AuthorCopyright}")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}". +// The form "{Major}.{Minor}.*" will automatically update the build and revision, +// and "{Major}.{Minor}.{Build}.*" will update just the revision. + +[assembly: AssemblyVersion("1.0.*")] + +// The following attributes are used to specify the signing key for the assembly, +// if desired. See the Mono documentation for more information about signing. + +//[assembly: AssemblyDelaySign(false)] +//[assembly: AssemblyKeyFile("")] diff --git a/RTM/RTM.Unity/RTM.Unity.csproj b/RTM/RTM.Unity/RTM.Unity.csproj new file mode 100644 index 0000000..51ba4ed --- /dev/null +++ b/RTM/RTM.Unity/RTM.Unity.csproj @@ -0,0 +1,225 @@ + + + + Debug + AnyCPU + {1E608FCD-9039-4FF7-8EE7-BA8B00E15D1C} + Library + RTM.Unity + RTM.Unity + v4.7 + 0.1.0 + + + true + full + false + bin\Debug + DEBUG; + prompt + 4 + false + + + true + bin\Release + prompt + 4 + false + + + + + ..\..\Libs\UnityEngine.dll + + + ..\..\Libs\websocket-sharp.dll + + + + + + Internal\AVIMCorePlugins.cs + + + Internal\IAVIMPlatformHooks.cs + + + Internal\Command\AVIMCommand.cs + + + Internal\Command\AVIMCommandRunner.cs + + + Internal\Command\AckCommand.cs + + + Internal\Command\ConversationCommand.cs + + + Internal\Command\IAVIMCommandRunner.cs + + + Internal\Command\MessageCommand.cs + + + Internal\Command\PatchCommand.cs + + + Internal\Command\ReadCommand.cs + + + Internal\Command\SessionCommand.cs + + + Internal\DataEngine\Controller\DateTimeEngine.cs + + + Internal\DataEngine\Controller\DictionaryEngine.cs + + + Internal\DataEngine\Controller\StringEngine.cs + + + Internal\Message\Subclassing\FreeStyleMessageClassInfo.cs + + + Internal\Message\Subclassing\FreeStyleMessageClassingController.cs + + + Internal\Message\Subclassing\IFreeStyleMessageClassingController.cs + + + Internal\Protocol\AVIMProtocol.cs + + + Internal\Router\AVRouterController.cs + + + Internal\Router\IAVRouterController.cs + + + Internal\Router\State\RouterState.cs + + + Internal\Timer\IAVTimer.cs + + + Internal\Timer\Unity\AVTimer.Unity.cs + + + Internal\WebSocket\IWebSocketClient.cs + + + Internal\WebSocket\Unity\DefaultWebSocketClient.Unity.cs + + + Public\AVIMAudioMessage.cs + + + Public\AVIMBinaryMessage.cs + + + Public\AVIMClient.cs + + + Public\AVIMConversation.cs + + + Public\AVIMConversationQuery.cs + + + Public\AVIMEnumerator.cs + + + Public\AVIMEventArgs.cs + + + Public\AVIMException.cs + + + Public\AVIMImageMessage.cs + + + Public\AVIMMessage.cs + + + Public\AVIMMessageClassNameAttribute.cs + + + Public\AVIMMessageFieldNameAttribute.cs + + + Public\AVIMMessageListener.cs + + + Public\AVIMNotice.cs + + + Public\AVIMRecalledMessage.cs + + + Public\AVIMSignature.cs + + + Public\AVIMTemporaryConversation.cs + + + Public\AVIMTextMessage.cs + + + Public\AVIMTypedMessage.cs + + + Public\AVIMTypedMessageTypeIntAttribute.cs + + + Public\AVRealtime.cs + + + Public\IAVIMListener.cs + + + Public\IAVIMMessage.cs + + + Public\ICacheEngine.cs + + + Public\ISignatureFactory.cs + + + Public\Listener\AVIMConversationListener.cs + + + Public\Listener\ConversationUnreadListener.cs + + + Public\Listener\GoAwayListener.cs + + + Public\Listener\MessagePatchListener.cs + + + Public\Listener\OfflineMessageListener.cs + + + Public\Listener\SessionListener.cs + + + Public\Unity\AVRealtimeBehavior.cs + + + + + Internal\WebSocket\Unity\websocket-sharp.dll + + + + + {A0D50BCB-E50E-4AAE-8E7D-24BF5AE33DAC} + Storage.Unity + + + + \ No newline at end of file diff --git a/RTM/Source/Internal/AVIMCorePlugins.cs b/RTM/Source/Internal/AVIMCorePlugins.cs new file mode 100644 index 0000000..859e0bc --- /dev/null +++ b/RTM/Source/Internal/AVIMCorePlugins.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LeanCloud.Realtime.Internal +{ + internal class AVIMCorePlugins + { + private static readonly AVIMCorePlugins instance = new AVIMCorePlugins(); + public static AVIMCorePlugins Instance + { + get + { + return instance; + } + } + + private readonly object mutex = new object(); + + private IAVRouterController routerController; + public IAVRouterController RouterController + { + get + { + lock (mutex) + { + routerController = routerController ?? new AVRouterController(); + return routerController; + } + } + internal set + { + lock (mutex) + { + routerController = value; + } + } + } + + + private IFreeStyleMessageClassingController freeStyleClassingController; + public IFreeStyleMessageClassingController FreeStyleClassingController + { + get + { + lock (mutex) + { + freeStyleClassingController = freeStyleClassingController ?? new FreeStyleMessageClassingController(); + return freeStyleClassingController; + } + } + } + } +} diff --git a/RTM/Source/Internal/Command/AVIMCommand.cs b/RTM/Source/Internal/Command/AVIMCommand.cs new file mode 100644 index 0000000..18300e5 --- /dev/null +++ b/RTM/Source/Internal/Command/AVIMCommand.cs @@ -0,0 +1,176 @@ +using LeanCloud; +using LeanCloud.Storage.Internal; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LeanCloud.Realtime.Internal +{ + /// + /// Command. + /// + public class AVIMCommand + { + protected readonly string cmd; + protected readonly string op; + protected string appId; + protected string peerId; + protected AVIMSignature signature; + protected readonly IDictionary arguments; + + public int TimeoutInSeconds { get; set; } + + protected readonly IDictionary estimatedData = new Dictionary(); + internal readonly object mutex = new object(); + internal static readonly object Mutex = new object(); + + public AVIMCommand() : + this(arguments: new Dictionary()) + { + + } + protected AVIMCommand(string cmd = null, + string op = null, + string appId = null, + string peerId = null, + AVIMSignature signature = null, + IDictionary arguments = null) + { + this.cmd = cmd; + this.op = op; + this.arguments = arguments == null ? new Dictionary() : arguments; + this.peerId = peerId; + this.signature = signature; + } + + protected AVIMCommand(AVIMCommand source, + string cmd = null, + string op = null, + string appId = null, + string peerId = null, + IDictionary arguments = null, + AVIMSignature signature = null) + { + if (source == null) + { + throw new ArgumentNullException("source", "Source can not be null"); + } + this.cmd = source.cmd; + this.op = source.op; + this.arguments = source.arguments; + this.peerId = source.peerId; + this.appId = source.appId; + this.signature = source.signature; + + if (cmd != null) + { + this.cmd = cmd; + } + if (op != null) + { + this.op = op; + } + if (arguments != null) + { + this.arguments = arguments; + } + if (peerId != null) + { + this.peerId = peerId; + } + if (appId != null) + { + this.appId = appId; + } + if (signature != null) + { + this.signature = signature; + } + } + + public AVIMCommand Command(string cmd) + { + return new AVIMCommand(this, cmd: cmd); + } + public AVIMCommand Option(string op) + { + return new AVIMCommand(this, op: op); + } + public AVIMCommand Argument(string key, object value) + { + lock (mutex) + { + this.arguments[key] = value; + return new AVIMCommand(this); + } + } + public AVIMCommand AppId(string appId) + { + this.appId = appId; + return new AVIMCommand(this, appId: appId); + } + + public AVIMCommand PeerId(string peerId) + { + this.peerId = peerId; + return new AVIMCommand(this, peerId: peerId); + } + + public AVIMCommand IDlize() + { + this.Argument("i", AVIMCommand.NextCmdId); + return this; + } + + public virtual IDictionary Encode() + { + lock (mutex) + { + estimatedData.Clear(); + estimatedData.Merge(arguments); + estimatedData.Add("cmd", cmd); + estimatedData.Add("appId", this.appId); + if (!string.IsNullOrEmpty(op)) + estimatedData.Add("op", op); + if (!string.IsNullOrEmpty(peerId)) + estimatedData.Add("peerId", peerId); + + return estimatedData; + } + } + + public virtual string EncodeJsonString() + { + var json = this.Encode(); + return Json.Encode(json); + } + + public bool IsValid + { + get + { + return !string.IsNullOrEmpty(this.cmd); + } + } + + private static Int32 lastCmdId = -65536; + internal static Int32 NextCmdId + { + get + { + lock (Mutex) + { + lastCmdId++; + + if (lastCmdId > ushort.MaxValue) + { + lastCmdId = -65536; + } + return lastCmdId; + } + } + } + } +} diff --git a/RTM/Source/Internal/Command/AVIMCommandRunner.cs b/RTM/Source/Internal/Command/AVIMCommandRunner.cs new file mode 100644 index 0000000..2052c46 --- /dev/null +++ b/RTM/Source/Internal/Command/AVIMCommandRunner.cs @@ -0,0 +1,92 @@ +using LeanCloud; +using LeanCloud.Storage.Internal; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace LeanCloud.Realtime.Internal +{ + public class AVIMCommandRunner : IAVIMCommandRunner + { + private readonly IWebSocketClient webSocketClient; + public AVIMCommandRunner(IWebSocketClient webSocketClient) + { + this.webSocketClient = webSocketClient; + } + + public void RunCommand(AVIMCommand command) + { + command.IDlize(); + var requestString = command.EncodeJsonString(); + AVRealtime.PrintLog("websocket=>" + requestString); + webSocketClient.Send(requestString); + } + + /// + /// + /// + /// + /// + /// + public Task>> RunCommandAsync(AVIMCommand command, CancellationToken cancellationToken = default(CancellationToken)) + { + var tcs = new TaskCompletionSource>>(); + + command.IDlize(); + + var requestString = command.EncodeJsonString(); + if (!command.IsValid) + { + requestString = "{}"; + } + AVRealtime.PrintLog("websocket=>" + requestString); + webSocketClient.Send(requestString); + var requestJson = command.Encode(); + + + Action onMessage = null; + onMessage = (response) => + { + //AVRealtime.PrintLog("response<=" + response); + var responseJson = Json.Parse(response) as IDictionary; + if (responseJson.Keys.Contains("i")) + { + if (requestJson["i"].ToString() == responseJson["i"].ToString()) + { + var result = new Tuple>(-1, responseJson); + if (responseJson.Keys.Contains("code")) + { + var errorCode = int.Parse(responseJson["code"].ToString()); + var reason = string.Empty; + int appCode = 0; + + if (responseJson.Keys.Contains("reason")) + { + reason = responseJson["reason"].ToString(); + } + if (responseJson.Keys.Contains("appCode")) + { + appCode = int.Parse(responseJson["appCode"].ToString()); + } + tcs.SetException(new AVIMException(errorCode, appCode, reason, null)); + } + if (tcs.Task.Exception == null) + { + tcs.SetResult(result); + } + webSocketClient.OnMessage -= onMessage; + } + else + { + + } + } + }; + webSocketClient.OnMessage += onMessage; + return tcs.Task; + } + } +} diff --git a/RTM/Source/Internal/Command/AckCommand.cs b/RTM/Source/Internal/Command/AckCommand.cs new file mode 100644 index 0000000..938f078 --- /dev/null +++ b/RTM/Source/Internal/Command/AckCommand.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LeanCloud.Realtime.Internal +{ + internal class AckCommand : AVIMCommand + { + public AckCommand() + : base(cmd: "ack") + { + + } + + public AckCommand(AVIMCommand source) + : base(source) + { + + } + + public AckCommand Message(IAVIMMessage message) + { + return new AckCommand() + .ConversationId(message.ConversationId) + .MessageId(message.Id); + } + + public AckCommand MessageId(string messageId) + { + if (string.IsNullOrEmpty(messageId)) + { + messageId = ""; + } + return new AckCommand(this.Argument("mid", messageId)); + } + + public AckCommand ConversationId(string conversationId) + { + if (string.IsNullOrEmpty(conversationId)) + { + conversationId = ""; + } + return new AckCommand(this.Argument("cid", conversationId)); + } + + public AckCommand FromTimeStamp(long startTimeStamp) + { + return new AckCommand(this.Argument("fromts", startTimeStamp)); + } + + public AckCommand ToTimeStamp(long endTimeStamp) + { + return new AckCommand(this.Argument("tots", endTimeStamp)); + } + + public AckCommand ReadAck() + { + return new AckCommand(this.Argument("read", true)); + } + } +} diff --git a/RTM/Source/Internal/Command/ConversationCommand.cs b/RTM/Source/Internal/Command/ConversationCommand.cs new file mode 100644 index 0000000..ab4de2d --- /dev/null +++ b/RTM/Source/Internal/Command/ConversationCommand.cs @@ -0,0 +1,129 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LeanCloud.Realtime.Internal +{ + internal class ConversationCommand : AVIMCommand + { + protected IList members; + public ConversationCommand() + : base(cmd: "conv") + { + + } + + public ConversationCommand(AVIMCommand source) + : base(source: source) + { + } + + public ConversationCommand Member(string clientId) + { + if (members == null) + { + members = new List(); + } + members.Add(clientId); + return Members(members); + } + + public ConversationCommand Members(IEnumerable members) + { + this.members = members.ToList(); + return new ConversationCommand(this.Argument("m", members)); + } + + public ConversationCommand Transient(bool isTransient) + { + return new ConversationCommand(this.Argument("transient", isTransient)); + } + + public ConversationCommand Unique(bool isUnique) + { + return new ConversationCommand(this.Argument("unique", isUnique)); + } + + public ConversationCommand Temporary(bool isTemporary) + { + return new ConversationCommand(this.Argument("tempConv", isTemporary)); + } + + public ConversationCommand TempConvTTL(double tempConvTTL) + { + return new ConversationCommand(this.Argument("tempConvTTL", tempConvTTL)); + } + + public ConversationCommand Attr(IDictionary attr) + { + return new ConversationCommand(this.Argument("attr", attr)); + } + + public ConversationCommand Set(string key, object value) + { + return new ConversationCommand(this.Argument(key, value)); + } + + public ConversationCommand ConversationId(string conversationId) + { + return new ConversationCommand(this.Argument("cid", conversationId)); + } + + public ConversationCommand Generate(AVIMConversation conversation) + { + var attr = conversation.EncodeAttributes(); + var cmd = new ConversationCommand() + .ConversationId(conversation.ConversationId) + .Attr(attr) + .Members(conversation.MemberIds) + .Transient(conversation.IsTransient) + .Temporary(conversation.IsTemporary); + + if (conversation.IsTemporary) + { + var ttl = (conversation.expiredAt.Value - DateTime.Now).TotalSeconds; + cmd = cmd.TempConvTTL(ttl); + } + + return cmd; + } + + public ConversationCommand Where(object encodedQueryString) + { + return new ConversationCommand(this.Argument("where", encodedQueryString)); + } + + public ConversationCommand Limit(int limit) + { + return new ConversationCommand(this.Argument("limit", limit)); + } + + public ConversationCommand Skip(int skip) + { + return new ConversationCommand(this.Argument("skip", skip)); + } + + public ConversationCommand Count() + { + return new ConversationCommand(this.Argument("count", 1)); + } + + public ConversationCommand Sort(string sort) + { + return new ConversationCommand(this.Argument("sort", sort)); + } + + public ConversationCommand TargetClientId(string targetClientId) + { + return new ConversationCommand(this.Argument("targetClientId", targetClientId)); + } + + public ConversationCommand QueryAllMembers(bool queryAllMembers) + { + return new ConversationCommand(this.Argument("queryAllMembers", queryAllMembers)); + } + + } +} diff --git a/RTM/Source/Internal/Command/IAVIMCommandRunner.cs b/RTM/Source/Internal/Command/IAVIMCommandRunner.cs new file mode 100644 index 0000000..a904f97 --- /dev/null +++ b/RTM/Source/Internal/Command/IAVIMCommandRunner.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace LeanCloud.Realtime.Internal +{ + public interface IAVIMCommandRunner + { + Task>> RunCommandAsync(AVIMCommand command, + CancellationToken cancellationToken = default(CancellationToken)); + + void RunCommand(AVIMCommand command); + } +} diff --git a/RTM/Source/Internal/Command/MessageCommand.cs b/RTM/Source/Internal/Command/MessageCommand.cs new file mode 100644 index 0000000..ec76d94 --- /dev/null +++ b/RTM/Source/Internal/Command/MessageCommand.cs @@ -0,0 +1,83 @@ +using LeanCloud.Storage.Internal; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LeanCloud.Realtime.Internal +{ + internal class MessageCommand : AVIMCommand + { + public MessageCommand() + : base(cmd: "direct") + { + + } + + public MessageCommand(AVIMCommand source) + : base(source: source) + { + + } + + public MessageCommand ConvId(string convId) + { + return new MessageCommand(this.Argument("cid", convId)); + } + + public MessageCommand Receipt(bool receipt) + { + return new MessageCommand(this.Argument("r", receipt)); + } + + public MessageCommand Transient(bool transient) + { + if (transient) return new MessageCommand(this.Argument("transient", transient)); + return new MessageCommand(this); + } + public MessageCommand Priority(int priority) + { + if (priority > 1) return new MessageCommand(this.Argument("level", priority)); + return new MessageCommand(this); + } + public MessageCommand Will(bool will) + { + if (will) return new MessageCommand(this.Argument("will", will)); + return new MessageCommand(this); + } + public MessageCommand Distinct(string token) + { + return new MessageCommand(this.Argument("dt", token)); + } + public MessageCommand Message(string msg) + { + return new MessageCommand(this.Argument("msg", msg)); + } + public MessageCommand BinaryEncode(bool binaryEncode) + { + return new MessageCommand(this.Argument("bin", binaryEncode)); + } + + public MessageCommand PushData(IDictionary pushData) + { + return new MessageCommand(this.Argument("pushData", Json.Encode(pushData))); + } + + public MessageCommand Mention(IEnumerable clientIds) + { + var mentionedMembers = clientIds.ToList(); + return new MessageCommand(this.Argument("mentionPids", mentionedMembers)); + } + + public MessageCommand MentionAll(bool mentionAll) + { + return new MessageCommand(this.Argument("mentionAll", mentionAll)); + } + + public MessageCommand Binary(byte[] data) + { + return new MessageCommand(this.Argument("binaryMsg", data)); + } + } +} diff --git a/RTM/Source/Internal/Command/PatchCommand.cs b/RTM/Source/Internal/Command/PatchCommand.cs new file mode 100644 index 0000000..113a0db --- /dev/null +++ b/RTM/Source/Internal/Command/PatchCommand.cs @@ -0,0 +1,98 @@ +using LeanCloud.Storage.Internal; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LeanCloud.Realtime.Internal +{ + internal class PatchCommand : AVIMCommand + { + + internal struct Patch + { + public string MessageId { get; set; } + public string ConvId { get; set; } + public string From { get; set; } + public long MetaTimestamp { get; set; } + public long PatchTimestamp { get; set; } + public string PatchData { get; set; } + public bool Recall { get; set; } + public byte[] BinaryData { get; set; } + public bool MentionAll { get; set; } + public IEnumerable MentionIds { get; set; } + + public IDictionary Encode() + { + return new Dictionary() + { + { "cid",this.ConvId}, + { "mid",this.MessageId}, + { "from",this.From}, + { "timestamp",this.MetaTimestamp}, + { "recall",this.Recall}, + { "data",this.PatchData}, + { "patchTimestamp",this.PatchTimestamp}, + { "binaryMsg",this.BinaryData}, + { "mentionAll",this.MentionAll}, + { "meintonPids",this.MentionIds} + } as IDictionary; + } + } + + public PatchCommand() + : base(cmd: "patch", op: "modify") + { + this.Patches = new List(); + } + + public PatchCommand(AVIMCommand source, ICollection sourcePatchs) + : base(source: source) + { + this.Patches = sourcePatchs; + } + + public ICollection Patches { get; set; } + + public IList> EncodePatches() + { + return this.Patches.Select(p => p.Encode().Trim()).ToList(); + } + + public PatchCommand Recall(IAVIMMessage message) + { + var patch = new Patch() + { + ConvId = message.ConversationId, + From = message.FromClientId, + MessageId = message.Id, + MetaTimestamp = message.ServerTimestamp, + Recall = true, + PatchTimestamp = DateTime.Now.ToUnixTimeStamp() + }; + + this.Patches.Add(patch); + this.Argument("patches", this.EncodePatches()); + return new PatchCommand(this, this.Patches); + } + + public PatchCommand Modify(IAVIMMessage oldMessage, IAVIMMessage newMessage) + { + var patch = new Patch() + { + ConvId = oldMessage.ConversationId, + From = oldMessage.FromClientId, + MessageId = oldMessage.Id, + MetaTimestamp = oldMessage.ServerTimestamp, + Recall = false, + PatchTimestamp = DateTime.Now.ToUnixTimeStamp(), + PatchData = newMessage.Serialize() + }; + + this.Patches.Add(patch); + this.Argument("patches", this.EncodePatches()); + return new PatchCommand(this, this.Patches); + } + } +} diff --git a/RTM/Source/Internal/Command/ReadCommand.cs b/RTM/Source/Internal/Command/ReadCommand.cs new file mode 100644 index 0000000..a627d00 --- /dev/null +++ b/RTM/Source/Internal/Command/ReadCommand.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LeanCloud.Realtime.Internal +{ + internal class ReadCommand : AVIMCommand + { + internal class ConvRead + { + internal string ConvId { get; set; } + internal string MessageId { get; set; } + internal long Timestamp { get; set; } + public override bool Equals(object obj) + { + ConvRead cr = obj as ConvRead; + return cr.ConvId == this.ConvId; + } + public override int GetHashCode() + { + return this.ConvId.GetHashCode() ^ this.MessageId.GetHashCode() ^ this.Timestamp.GetHashCode(); + } + } + + public ReadCommand() + : base(cmd: "read") + { + + } + + public ReadCommand(AVIMCommand source) + : base(source) + { + + } + + public ReadCommand ConvId(string convId) + { + return new ReadCommand(this.Argument("cid", convId)); + } + + public ReadCommand ConvIds(IEnumerable convIds) + { + if (convIds != null) + { + if (convIds.Count() > 0) + { + return new ReadCommand(this.Argument("cids", convIds.ToList())); + } + } + return this; + + } + + public ReadCommand Conv(ConvRead conv) + { + return Convs(new ConvRead[] { conv }); + } + + public ReadCommand Convs(IEnumerable convReads) + { + if (convReads != null) + { + if (convReads.Count() > 0) + { + IList> payload = new List>(); + + foreach (var convRead in convReads) + { + var convDic = new Dictionary(); + convDic.Add("cid", convRead.ConvId); + if (!string.IsNullOrEmpty(convRead.MessageId)) + { + convDic.Add("mid", convRead.MessageId); + } + if (convRead.Timestamp != 0) + { + convDic.Add("timestamp", convRead.Timestamp); + } + payload.Add(convDic); + } + + return new ReadCommand(this.Argument("convs", payload)); + } + } + return this; + } + } +} diff --git a/RTM/Source/Internal/Command/SessionCommand.cs b/RTM/Source/Internal/Command/SessionCommand.cs new file mode 100644 index 0000000..f18b7b9 --- /dev/null +++ b/RTM/Source/Internal/Command/SessionCommand.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LeanCloud.Realtime.Internal +{ + internal class SessionCommand : AVIMCommand + { + static readonly int MESSAGE_RECALL_AND_MODIFY = 0x1; + + public SessionCommand() + : base(cmd: "session") + { + arguments.Add("configBitmap", MESSAGE_RECALL_AND_MODIFY); + } + + public SessionCommand(AVIMCommand source) + :base(source: source) + { + + } + + public SessionCommand UA(string ua) + { + return new SessionCommand(this.Argument("ua", ua)); + } + + public SessionCommand Tag(string tag) + { + if (string.IsNullOrEmpty(tag)) return new SessionCommand(this); + return new SessionCommand(this.Argument("tag", tag)); + } + + public SessionCommand DeviceId(string deviceId) + { + if (string.IsNullOrEmpty(deviceId)) return new SessionCommand(this); + return new SessionCommand(this.Argument("deviceId", deviceId)); + } + + public SessionCommand R(int r) + { + return new SessionCommand(this.Argument("r", r)); + } + + public SessionCommand SessionToken(string st) + { + return new SessionCommand(this.Argument("st", st)); + } + + public SessionCommand SessionPeerIds(IEnumerable sessionPeerIds) + { + return new SessionCommand(this.Argument("sessionPeerIds", sessionPeerIds.ToList())); + } + } +} diff --git a/RTM/Source/Internal/DataEngine/Controller/DateTimeEngine.cs b/RTM/Source/Internal/DataEngine/Controller/DateTimeEngine.cs new file mode 100644 index 0000000..25c920a --- /dev/null +++ b/RTM/Source/Internal/DataEngine/Controller/DateTimeEngine.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LeanCloud.Realtime.Internal +{ + internal enum UnixTimeStampUnit + { + Second = 1, + Milisecond = 1000, + } + internal static class DateTimeEngine + { + public static long ToUnixTimeStamp(this DateTime date, UnixTimeStampUnit unit = UnixTimeStampUnit.Milisecond) + { + long unixTimestamp = (long)(date.ToUniversalTime().Subtract(new DateTime(1970, 1, 1))).TotalSeconds; + return (unixTimestamp * (int)unit); + } + + public static DateTime ToDateTime(this long timestamp, UnixTimeStampUnit unit = UnixTimeStampUnit.Milisecond) + { + var timespan = timestamp * 1000 / (int)(unit); + DateTime dtDateTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); + dtDateTime = dtDateTime.AddMilliseconds(timespan).ToLocalTime(); + return dtDateTime; + } + } +} diff --git a/RTM/Source/Internal/DataEngine/Controller/DictionaryEngine.cs b/RTM/Source/Internal/DataEngine/Controller/DictionaryEngine.cs new file mode 100644 index 0000000..a1da600 --- /dev/null +++ b/RTM/Source/Internal/DataEngine/Controller/DictionaryEngine.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LeanCloud.Realtime.Internal +{ + internal static class DictionaryEngine + { + internal static IDictionary Merge(this IDictionary dataLeft, IDictionary dataRight) + { + if (dataRight == null) + return dataLeft; + foreach (var kv in dataRight) + { + if (dataLeft.ContainsKey(kv.Key)) + { + dataLeft[kv.Key] = kv.Value; + } + else + { + dataLeft.Add(kv); + } + } + return dataLeft; + } + + internal static object Grab(this IDictionary data, string path) + { + var keys = path.Split('.').ToList(); + if (keys.Count == 1) return data[keys[0]]; + + var deep = data[keys[0]] as IDictionary; + + keys.RemoveAt(0); + string deepPath = string.Join(".", keys.ToArray()); + + return Grab(deep, deepPath); + } + + internal static IDictionary Trim(this IDictionary data) + { + return data.Where(kvp => kvp.Value != null).ToDictionary(k => k.Key, v => v.Value); + } + } +} diff --git a/RTM/Source/Internal/DataEngine/Controller/StringEngine.cs b/RTM/Source/Internal/DataEngine/Controller/StringEngine.cs new file mode 100644 index 0000000..62e902a --- /dev/null +++ b/RTM/Source/Internal/DataEngine/Controller/StringEngine.cs @@ -0,0 +1,36 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Text; + +namespace LeanCloud.Realtime.Internal +{ + internal static class StringEngine + { + internal static string Random(this string str, int length) + { + const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz"; + var random = new Random(); + return new string(Enumerable.Repeat(chars, length) + .Select(s => s[random.Next(s.Length)]).ToArray()); + } + + internal static string TempConvId(this IEnumerable objs) + { + var orderedBase64Strs = objs.Select(obj => Encoding.UTF8.ToBase64(obj.ToString())).OrderBy(a => a, StringComparer.Ordinal).ToArray(); + return "_tmp:" + string.Join("$", orderedBase64Strs); + } + + internal static string ToBase64(this System.Text.Encoding encoding, string text) + { + if (text == null) + { + return null; + } + + byte[] textAsBytes = encoding.GetBytes(text); + return Convert.ToBase64String(textAsBytes); + } + } +} diff --git a/RTM/Source/Internal/IAVIMPlatformHooks.cs b/RTM/Source/Internal/IAVIMPlatformHooks.cs new file mode 100644 index 0000000..53d737c --- /dev/null +++ b/RTM/Source/Internal/IAVIMPlatformHooks.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LeanCloud.Realtime.Internal +{ + interface IAVIMPlatformHooks + { + IWebSocketClient WebSocketClient { get; } + + string ua { get; } + } +} diff --git a/RTM/Source/Internal/Message/Subclassing/FreeStyleMessageClassInfo.cs b/RTM/Source/Internal/Message/Subclassing/FreeStyleMessageClassInfo.cs new file mode 100644 index 0000000..e1a1e56 --- /dev/null +++ b/RTM/Source/Internal/Message/Subclassing/FreeStyleMessageClassInfo.cs @@ -0,0 +1,75 @@ +using LeanCloud.Storage.Internal; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; + +namespace LeanCloud.Realtime.Internal +{ + internal class FreeStyleMessageClassInfo + { + public TypeInfo TypeInfo { get; private set; } + public IDictionary PropertyMappings { get; private set; } + private ConstructorInfo Constructor { get; set; } + //private MethodInfo ValidateMethod { get; set; } + + public int TypeInt { get; set; } + + public FreeStyleMessageClassInfo(Type type, ConstructorInfo constructor) + { + TypeInfo = type.GetTypeInfo(); + Constructor = constructor; + PropertyMappings = ReflectionHelpers.GetProperties(type) + .Select(prop => Tuple.Create(prop, prop.GetCustomAttribute(true))) + .Where(t => t.Item2 != null) + .Select(t => Tuple.Create(t.Item1, t.Item2.FieldName)) + .ToDictionary(t => t.Item1.Name, t => t.Item2); + } + public bool Validate(string msgStr) + { + var instance = Instantiate(msgStr); + if (instance is AVIMTypedMessage) + { + try + { + var msgDic = Json.Parse(msgStr) as IDictionary; + if (msgDic != null) + { + if (msgDic.ContainsKey(AVIMProtocol.LCTYPE)) + { + return msgDic[AVIMProtocol.LCTYPE].ToString() == TypeInt.ToString(); + } + } + } + catch (Exception ex) + { + if (ex is ArgumentException) + { + return instance.Validate(msgStr); + } + } + + } + return instance.Validate(msgStr); + } + + public IAVIMMessage Instantiate(string msgStr) + { + var rtn = (IAVIMMessage)Constructor.Invoke(null); + return rtn; + } + + public static string GetMessageClassName(TypeInfo type) + { + var attribute = type.GetCustomAttribute(); + return attribute != null ? attribute.ClassName : null; + } + + public static int GetTypedInteger(TypeInfo type) + { + var attribute = type.GetCustomAttribute(); + return attribute != null ? attribute.TypeInteger : 0; + } + } +} diff --git a/RTM/Source/Internal/Message/Subclassing/FreeStyleMessageClassingController.cs b/RTM/Source/Internal/Message/Subclassing/FreeStyleMessageClassingController.cs new file mode 100644 index 0000000..04bd105 --- /dev/null +++ b/RTM/Source/Internal/Message/Subclassing/FreeStyleMessageClassingController.cs @@ -0,0 +1,210 @@ +using LeanCloud.Storage.Internal; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading; + +namespace LeanCloud.Realtime.Internal +{ + internal class FreeStyleMessageClassingController : IFreeStyleMessageClassingController + { + private static readonly string messageClassName = "_AVIMMessage"; + private readonly IDictionary registeredInterfaces; + private readonly ReaderWriterLockSlim mutex; + + public FreeStyleMessageClassingController() + { + mutex = new ReaderWriterLockSlim(); + registeredInterfaces = new Dictionary(); + } + + public Type GetType(IDictionary msg) + { + throw new NotImplementedException(); + } + + public IAVIMMessage Instantiate(string msgStr, IDictionary buildInData) + { + FreeStyleMessageClassInfo info = null; + mutex.EnterReadLock(); + bool bin = false; + if (buildInData.ContainsKey("bin")) + { + bool.TryParse(buildInData["bin"].ToString(), out bin); + } + + if (bin) + { + var binMessage = new AVIMBinaryMessage(); + this.DecodeProperties(binMessage, buildInData); + return binMessage; + } + + var reverse = registeredInterfaces.Values.Reverse(); + foreach (var subInterface in reverse) + { + if (subInterface.Validate(msgStr)) + { + info = subInterface; + break; + } + } + + mutex.ExitReadLock(); + + var message = info != null ? info.Instantiate(msgStr) : new AVIMMessage(); + + this.DecodeProperties(message, buildInData); + + message.Deserialize(msgStr); + + return message; + } + + public IAVIMMessage DecodeProperties(IAVIMMessage message, IDictionary buildInData) + { + long timestamp; + if (buildInData.ContainsKey("timestamp")) + { + if (long.TryParse(buildInData["timestamp"].ToString(), out timestamp)) + { + message.ServerTimestamp = timestamp; + } + } + long ackAt; + if (buildInData.ContainsKey("ackAt")) + { + if (long.TryParse(buildInData["ackAt"].ToString(), out ackAt)) + { + message.RcpTimestamp = ackAt; + } + } + + if (buildInData.ContainsKey("from")) + { + message.FromClientId = buildInData["from"].ToString(); + } + if (buildInData.ContainsKey("msgId")) + { + message.Id = buildInData["msgId"].ToString(); + } + if (buildInData.ContainsKey("cid")) + { + message.ConversationId = buildInData["cid"].ToString(); + } + if (buildInData.ContainsKey("fromPeerId")) + { + message.FromClientId = buildInData["fromPeerId"].ToString(); + } + if (buildInData.ContainsKey("id")) + { + message.Id = buildInData["id"].ToString(); + } + if (buildInData.ContainsKey("mid")) + { + message.Id = buildInData["mid"].ToString(); + } + if (buildInData.ContainsKey("mentionPids")) + { + message.MentionList = AVDecoder.Instance.DecodeList(buildInData["mentionPids"]); + } + if (buildInData.TryGetValue("patchTimestamp", out object patchTimestampObj)) { + if (long.TryParse(patchTimestampObj.ToString(), out long patchTimestamp)) { + message.UpdatedAt = patchTimestamp; + } + } + + bool mentionAll; + if (buildInData.ContainsKey("mentionAll")) + { + if (bool.TryParse(buildInData["mentionAll"].ToString(), out mentionAll)) + { + message.MentionAll = mentionAll; + } + } + return message; + } + + public IDictionary EncodeProperties(IAVIMMessage subclass) + { + var type = subclass.GetType(); + var result = new Dictionary(); + var className = GetClassName(type); + var typeInt = GetTypeInt(type); + var propertMappings = GetPropertyMappings(className); + foreach (var propertyPair in propertMappings) + { + var propertyInfo = ReflectionHelpers.GetProperty(type, propertyPair.Key); + var operation = propertyInfo.GetValue(subclass, null); + if (operation != null) + result[propertyPair.Value] = PointerOrLocalIdEncoder.Instance.Encode(operation); + } + if (typeInt != 0) + { + result[AVIMProtocol.LCTYPE] = typeInt; + } + return result; + } + + public bool IsTypeValid(IDictionary msg, Type type) + { + return true; + } + + public void RegisterSubclass(Type type) + { + TypeInfo typeInfo = type.GetTypeInfo(); + + if (!typeof(IAVIMMessage).GetTypeInfo().IsAssignableFrom(typeInfo)) + { + throw new ArgumentException("Cannot register a type that is not a implementation of IAVIMMessage"); + } + var className = GetClassName(type); + var typeInt = GetTypeInt(type); + try + { + mutex.EnterWriteLock(); + ConstructorInfo constructor = type.FindConstructor(); + if (constructor == null) + { + throw new ArgumentException("Cannot register a type that does not implement the default constructor!"); + } + var classInfo = new FreeStyleMessageClassInfo(type, constructor); + if (typeInt != 0) + { + classInfo.TypeInt = typeInt; + } + registeredInterfaces[className] = classInfo; + } + finally + { + mutex.ExitWriteLock(); + } + } + public String GetClassName(Type type) + { + return type == typeof(IAVIMMessage) + ? messageClassName + : FreeStyleMessageClassInfo.GetMessageClassName(type.GetTypeInfo()); + } + public int GetTypeInt(Type type) + { + return type == typeof(AVIMTypedMessage) ? 0 : FreeStyleMessageClassInfo.GetTypedInteger(type.GetTypeInfo()); + } + public IDictionary GetPropertyMappings(String className) + { + FreeStyleMessageClassInfo info = null; + mutex.EnterReadLock(); + registeredInterfaces.TryGetValue(className, out info); + if (info == null) + { + registeredInterfaces.TryGetValue(messageClassName, out info); + } + mutex.ExitReadLock(); + + return info.PropertyMappings; + } + } +} diff --git a/RTM/Source/Internal/Message/Subclassing/IFreeStyleMessageClassingController.cs b/RTM/Source/Internal/Message/Subclassing/IFreeStyleMessageClassingController.cs new file mode 100644 index 0000000..0fc3912 --- /dev/null +++ b/RTM/Source/Internal/Message/Subclassing/IFreeStyleMessageClassingController.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace LeanCloud.Realtime.Internal +{ + interface IFreeStyleMessageClassingController + { + bool IsTypeValid(IDictionary msg, Type type); + void RegisterSubclass(Type t); + IAVIMMessage Instantiate(string msgStr,IDictionary buildInData); + IDictionary EncodeProperties(IAVIMMessage subclass); + Type GetType(IDictionary msg); + String GetClassName(Type type); + IDictionary GetPropertyMappings(String className); + } +} diff --git a/RTM/Source/Internal/Protocol/AVIMProtocol.cs b/RTM/Source/Internal/Protocol/AVIMProtocol.cs new file mode 100644 index 0000000..6c71044 --- /dev/null +++ b/RTM/Source/Internal/Protocol/AVIMProtocol.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LeanCloud.Realtime.Internal +{ + internal class AVIMProtocol + { + #region msg format + static internal readonly string LCTYPE = "_lctype"; + static internal readonly string LCFILE = "_lcfile"; + static internal readonly string LCTEXT = "_lctext"; + static internal readonly string LCATTRS = "_lcattrs"; + static internal readonly string LCLOC = "_lcloc"; + #endregion + } +} diff --git a/RTM/Source/Internal/Router/AVRouterController.cs b/RTM/Source/Internal/Router/AVRouterController.cs new file mode 100644 index 0000000..dc47848 --- /dev/null +++ b/RTM/Source/Internal/Router/AVRouterController.cs @@ -0,0 +1,173 @@ +using LeanCloud.Storage.Internal; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace LeanCloud.Realtime.Internal +{ + internal class AVRouterController : IAVRouterController + { + const string routerUrl = "http://router.g0.push.leancloud.cn/v1/route?appId={0}"; + const string routerKey = "LeanCloud_RouterState"; + public Task GetAsync(string pushRouter = null, bool secure = true, CancellationToken cancellationToken = default(CancellationToken)) + { + //return Task.FromResult(new PushRouterState() + //{ + // server = "wss://rtm57.leancloud.cn/" + //}); + return LoadAysnc(cancellationToken).OnSuccess(_ => + { + var cache = _.Result; + var task = Task.FromResult(cache); + + if (cache == null || cache.expire < DateTime.Now.ToUnixTimeStamp()) + { + task = QueryAsync(pushRouter, secure, cancellationToken); + } + + return task; + }).Unwrap(); + } + + /// + /// 清理地址缓存 + /// + /// The cache. + public Task ClearCache() { + var tcs = new TaskCompletionSource(); + AVPlugins.Instance.StorageController.LoadAsync().ContinueWith(t => { + if (t.IsFaulted) { + tcs.SetResult(true); + } else { + var storage = t.Result; + if (storage.ContainsKey(routerKey)) { + storage.RemoveAsync(routerKey).ContinueWith(_ => tcs.SetResult(true)); + } else { + tcs.SetResult(true); + } + } + }); + return tcs.Task; + } + + Task LoadAysnc(CancellationToken cancellationToken) + { + try + { + return AVPlugins.Instance.StorageController.LoadAsync().OnSuccess(_ => + { + var currentCache = _.Result; + object routeCacheStr = null; + if (currentCache.TryGetValue(routerKey, out routeCacheStr)) + { + var routeCache = routeCacheStr as IDictionary; + var routerState = new PushRouterState() + { + groupId = routeCache["groupId"] as string, + server = routeCache["server"] as string, + secondary = routeCache["secondary"] as string, + ttl = long.Parse(routeCache["ttl"].ToString()), + expire = long.Parse(routeCache["expire"].ToString()), + source = "localCache" + }; + return routerState; + } + return null; + }); + } + catch + { + return Task.FromResult(null); + } + } + + Task QueryAsync(string pushRouter, bool secure, CancellationToken cancellationToken) + { + var routerHost = pushRouter; + if (routerHost == null) { + var appRouter = AVPlugins.Instance.AppRouterController.Get(); + routerHost = string.Format("https://{0}/v1/route?appId={1}", appRouter.RealtimeRouterServer, AVClient.CurrentConfiguration.ApplicationId) ?? appRouter.RealtimeRouterServer ?? string.Format(routerUrl, AVClient.CurrentConfiguration.ApplicationId); + } + AVRealtime.PrintLog($"router: {routerHost}"); + AVRealtime.PrintLog($"push: {pushRouter}"); + if (!string.IsNullOrEmpty(pushRouter)) + { + var rtmUri = new Uri(pushRouter); + if (!string.IsNullOrEmpty(rtmUri.Scheme)) + { + var url = new Uri(rtmUri, "v1/route").ToString(); + routerHost = string.Format("{0}?appId={1}", url, AVClient.CurrentConfiguration.ApplicationId); + } + else + { + routerHost = string.Format("https://{0}/v1/route?appId={1}", pushRouter, AVClient.CurrentConfiguration.ApplicationId); + } + } + if (secure) + { + routerHost += "&secure=1"; + } + + AVRealtime.PrintLog("use push router url:" + routerHost); + + return AVClient.RequestAsync(uri: new Uri(routerHost), + method: "GET", + headers: null, + data: null, + contentType: "application/json", + cancellationToken: CancellationToken.None).ContinueWith(t => + { + if (t.Exception != null) + { + var innnerException = t.Exception.InnerException; + AVRealtime.PrintLog(innnerException.Message); + throw innnerException; + } + var httpStatus = (int)t.Result.Item1; + if (httpStatus != 200) + { + return null; + } + try + { + var result = t.Result.Item2; + + var routerState = Json.Parse(result) as IDictionary; + if (routerState.Keys.Count == 0) + { + throw new KeyNotFoundException("Can not get websocket url from server,please check the appId."); + } + var ttl = long.Parse(routerState["ttl"].ToString()); + var expire = DateTime.Now.AddSeconds(ttl); + routerState["expire"] = expire.ToUnixTimeStamp(); + + //save to local cache async. + AVPlugins.Instance.StorageController.LoadAsync().OnSuccess(storage => storage.Result.AddAsync(routerKey, routerState)); + var routerStateObj = new PushRouterState() + { + groupId = routerState["groupId"] as string, + server = routerState["server"] as string, + secondary = routerState["secondary"] as string, + ttl = long.Parse(routerState["ttl"].ToString()), + expire = expire.ToUnixTimeStamp(), + source = "online" + }; + + return routerStateObj; + } + catch (Exception e) + { + if (e is KeyNotFoundException) + { + throw e; + } + return null; + } + + }); + } + } +} diff --git a/RTM/Source/Internal/Router/IAVRouterController.cs b/RTM/Source/Internal/Router/IAVRouterController.cs new file mode 100644 index 0000000..b7eebfb --- /dev/null +++ b/RTM/Source/Internal/Router/IAVRouterController.cs @@ -0,0 +1,13 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace LeanCloud.Realtime.Internal +{ + interface IAVRouterController + { + Task GetAsync(string pushRouter, bool secure, CancellationToken cancellationToken = default(CancellationToken)); + Task ClearCache(); + } +} diff --git a/RTM/Source/Internal/Router/State/RouterState.cs b/RTM/Source/Internal/Router/State/RouterState.cs new file mode 100644 index 0000000..66eaea9 --- /dev/null +++ b/RTM/Source/Internal/Router/State/RouterState.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LeanCloud.Realtime.Internal +{ + internal class PushRouterState + { + public string groupId { get; internal set; } + public string server { get; internal set; } + public long ttl { get; internal set; } + public long expire { get; internal set; } + public string secondary { get; internal set; } + public string groupUrl { get; internal set; } + + public string source { get; internal set; } + } +} diff --git a/RTM/Source/Internal/Timer/IAVTimer.cs b/RTM/Source/Internal/Timer/IAVTimer.cs new file mode 100644 index 0000000..5573041 --- /dev/null +++ b/RTM/Source/Internal/Timer/IAVTimer.cs @@ -0,0 +1,49 @@ +using System; +namespace LeanCloud.Realtime.Internal +{ + public interface IAVTimer + { + /// + /// Start this timer. + /// + void Start(); + + /// + /// Stop this timer. + /// + void Stop(); + + bool Enabled { get; set; } + + /// + /// The number of milliseconds between timer events. + /// + /// The interval. + double Interval { get; set; } + + /// + /// 已经执行了多少次 + /// + long Executed { get; } + + /// + /// Occurs when elapsed. + /// + event EventHandler Elapsed; + } + /// + /// Timer event arguments. + /// + public class TimerEventArgs : EventArgs + { + public TimerEventArgs(DateTime signalTime) + { + SignalTime = signalTime; + } + public DateTime SignalTime + { + get; + private set; + } + } +} diff --git a/RTM/Source/Internal/Timer/Portable/AVTimer.Portable.cs b/RTM/Source/Internal/Timer/Portable/AVTimer.Portable.cs new file mode 100644 index 0000000..e981dbd --- /dev/null +++ b/RTM/Source/Internal/Timer/Portable/AVTimer.Portable.cs @@ -0,0 +1,107 @@ +using System; +using System.Threading.Tasks; +using System.Threading; + +namespace LeanCloud.Realtime.Internal +{ + internal delegate void TimerCallback(); + + internal sealed class Timer : CancellationTokenSource, IDisposable + { + TimerCallback exe; + int Interval { get; set; } + + internal Timer(TimerCallback callback, int interval, bool enable) + { + exe = callback; + Interval = interval; + + Enabled = enable; + Execute(); + } + + Task Execute() + { + if (Enabled) + return Task.Delay(Interval).ContinueWith(t => + { + if (!Enabled) + return null; + exe(); + return this.Execute(); + }); + else + return Task.FromResult(0); + } + + volatile bool enabled; + public bool Enabled + { + get { + return enabled; + } set { + enabled = value; + } + } + } + + public class AVTimer : IAVTimer + { + public AVTimer() + { + + } + + Timer timer; + + public bool Enabled + { + get + { + return timer.Enabled; + } + set + { + timer.Enabled = value; + } + } + + public double Interval + { + get; set; + } + + long executed; + + public long Executed + { + get + { + return executed; + } + + internal set + { + executed = value; + } + } + + public void Start() + { + if (timer == null) + { + timer = new Timer(() => + { + Elapsed(this, new TimerEventArgs(DateTime.Now)); + }, (int)Interval, true); + } + } + + public void Stop() + { + if (timer != null) timer.Enabled = false; + } + + public event EventHandler Elapsed; + } +} diff --git a/RTM/Source/Internal/Timer/Unity/AVTimer.Unity.cs b/RTM/Source/Internal/Timer/Unity/AVTimer.Unity.cs new file mode 100644 index 0000000..8298433 --- /dev/null +++ b/RTM/Source/Internal/Timer/Unity/AVTimer.Unity.cs @@ -0,0 +1,82 @@ +using System; +using System.Timers; + +namespace LeanCloud.Realtime.Internal +{ + public class AVTimer : IAVTimer + { + public AVTimer() + { + timer = new Timer(); + } + + Timer timer; + + public bool Enabled + { + get + { + return timer.Enabled; + } + set + { + timer.Enabled = value; + } + } + + public double Interval + { + get + { + return timer.Interval; + } + set + { + timer.Interval = value; + } + } + + long executed; + + public long Executed + { + get + { + return executed; + } + + internal set + { + executed = value; + } + } + + public void Start() + { + timer.Start(); + } + + public void Stop() + { + timer.Stop(); + } + + public event EventHandler Elapsed + { + add + { + timer.Elapsed += (object sender, ElapsedEventArgs e) => + { + value(this, new TimerEventArgs(e.SignalTime)); + }; + } + remove + { + timer.Elapsed -= (object sender, ElapsedEventArgs e) => + { + value(this, new TimerEventArgs(e.SignalTime)); + }; ; + } + } + } +} diff --git a/RTM/Source/Internal/WebSocket/IWebSocketClient.cs b/RTM/Source/Internal/WebSocket/IWebSocketClient.cs new file mode 100644 index 0000000..175ffa0 --- /dev/null +++ b/RTM/Source/Internal/WebSocket/IWebSocketClient.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace LeanCloud.Realtime.Internal +{ + /// + /// LeanCloud WebSocket 客户端接口 + /// + public interface IWebSocketClient + { + /// + /// 客户端 WebSocket 长连接是否打开 + /// + bool IsOpen { get; } + + /// + /// WebSocket 长连接关闭时触发的事件回调 + /// + event Action OnClosed; + + /// + /// 云端发送数据包给客户端,WebSocket 接受到时触发的事件回调 + /// + event Action OnMessage; + + /// + /// 客户端 WebSocket 长连接成功打开时,触发的事件回调 + /// + event Action OnOpened; + + /// + /// 主动关闭连接 + /// + void Close(); + + void Disconnect(); + + /// + /// 打开连接 + /// + /// wss 地址 + /// 子协议 + void Open(string url, string protocol = null); + /// + /// 发送数据包的接口 + /// + /// + void Send(string message); + + Task Connect(string url, string protocol = null); + } +} diff --git a/RTM/Source/Internal/WebSocket/NetCore/DefaultWebSocketClient.NetCore.cs b/RTM/Source/Internal/WebSocket/NetCore/DefaultWebSocketClient.NetCore.cs new file mode 100644 index 0000000..bf27232 --- /dev/null +++ b/RTM/Source/Internal/WebSocket/NetCore/DefaultWebSocketClient.NetCore.cs @@ -0,0 +1,174 @@ +using System; +using System.Net.WebSockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace LeanCloud.Realtime.Internal +{ + public class DefaultWebSocketClient : IWebSocketClient + { + private const int ReceiveChunkSize = 1024; + private const int SendChunkSize = 1024; + + private ClientWebSocket _ws; + private Uri _uri; + private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); + private readonly CancellationToken _cancellationToken; + + /// + /// Occurs when on closed. + /// + public event Action OnClosed; + /// + /// Occurs when on error. + /// + public event Action OnError; + /// + /// Occurs when on log. + /// + public event Action OnLog; + /// + /// Occurs when on opened. + /// + public event Action OnOpened; + + public bool IsOpen => _ws.State == WebSocketState.Open; + + public DefaultWebSocketClient() + { + _ws = NewWebSocket(); + _cancellationToken = _cancellationTokenSource.Token; + } + + public event Action OnMessage; + + private async void StartListen() + { + var buffer = new byte[8192]; + + try + { + while (_ws.State == WebSocketState.Open) + { + var stringResult = new StringBuilder(); + + WebSocketReceiveResult result; + do + { + result = await _ws.ReceiveAsync(new ArraySegment(buffer), _cancellationToken); + + if (result.MessageType == WebSocketMessageType.Close) + { + await + _ws.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); + CallOnDisconnected(); + } + else + { + var str = Encoding.UTF8.GetString(buffer, 0, result.Count); + stringResult.Append(str); + } + + } while (!result.EndOfMessage); + + CallOnMessage(stringResult); + + } + } + catch (Exception) + { + CallOnDisconnected(); + } + finally + { + _ws.Dispose(); + } + } + + private void CallOnMessage(StringBuilder stringResult) + { + if (OnMessage != null) + RunInTask(() => OnMessage(stringResult.ToString())); + } + + private void CallOnDisconnected() + { + AVRealtime.PrintLog("PCL websocket closed without parameters."); + if (OnClosed != null) + RunInTask(() => this.OnClosed(0, "", "")); + } + + private void CallOnConnected() + { + if (OnOpened != null) + RunInTask(() => this.OnOpened()); + } + + private static void RunInTask(Action action) + { + Task.Factory.StartNew(action); + } + + public async void Close() + { + if (_ws != null) + { + try + { + await _ws.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); + CallOnDisconnected(); + } + catch (Exception ex) + { + CallOnDisconnected(); + } + } + } + + public void Disconnect() { + _ws?.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); + } + + public async void Open(string url, string protocol = null) + { + _uri = new Uri(url); + if (_ws.State == WebSocketState.Open || _ws.State == WebSocketState.Connecting) + { + _ws = NewWebSocket(); + } + try + { + await _ws.ConnectAsync(_uri, _cancellationToken); + CallOnConnected(); + StartListen(); + } + catch (Exception ex) + { + if (ex is ObjectDisposedException) + { + OnError($"can NOT connect server with url: {url}"); + } + } + } + + public async void Send(string message) + { + if (_ws.State != WebSocketState.Open) + { + throw new Exception("Connection is not open."); + } + + var encoded = Encoding.UTF8.GetBytes(message); + var buffer = new ArraySegment(encoded, 0, encoded.Length); + await _ws.SendAsync(buffer, WebSocketMessageType.Text, true, _cancellationToken); + } + + ClientWebSocket NewWebSocket() + { + var result = new ClientWebSocket(); + result.Options.KeepAliveInterval = TimeSpan.FromSeconds(20); + return result; + } + } +} \ No newline at end of file diff --git a/RTM/Source/Internal/WebSocket/NetFx45/WebSocketClient.NetFx45.cs b/RTM/Source/Internal/WebSocket/NetFx45/WebSocketClient.NetFx45.cs new file mode 100644 index 0000000..dfa5ac9 --- /dev/null +++ b/RTM/Source/Internal/WebSocket/NetFx45/WebSocketClient.NetFx45.cs @@ -0,0 +1,80 @@ +using System; +using Websockets; +using System.Net.WebSockets; +using LeanCloud.Realtime.Internal; + +namespace LeanCloud.Realtime +{ + public class WebSocketClient: IWebSocketClient + { + internal readonly IWebSocketConnection connection; + public WebSocketClient() + { + Websockets.Net.WebsocketConnection.Link(); + connection = WebSocketFactory.Create(); + } + + public event Action OnClosed; + public event Action OnError; + public event Action OnLog; + + public event Action OnOpened + { + add + { + connection.OnOpened += value; + } + remove + { + connection.OnOpened -= value; + } + } + + public event Action OnMessage + { + add + { + connection.OnMessage += value; + } + remove + { + connection.OnMessage -= value; + } + } + + public bool IsOpen + { + get + { + return connection.IsOpen; + } + } + + public void Close() + { + if (connection != null) + { + connection.Close(); + } + } + + public void Open(string url, string protocol = null) + { + if (connection != null) + { + connection.Open(url, protocol); + } + } + + public void Send(string message) + { + if (connection != null) + { + if (this.IsOpen) + { + connection.Send(message); + } + } + } + } +} diff --git a/RTM/Source/Internal/WebSocket/Portable/DefaultWebSocketClient.Portable.cs b/RTM/Source/Internal/WebSocket/Portable/DefaultWebSocketClient.Portable.cs new file mode 100644 index 0000000..85face2 --- /dev/null +++ b/RTM/Source/Internal/WebSocket/Portable/DefaultWebSocketClient.Portable.cs @@ -0,0 +1,164 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Websockets; + +namespace LeanCloud.Realtime.Internal +{ + /// + /// LeanCloud Realtime SDK for .NET Portable 内置默认的 WebSocketClient + /// + public class DefaultWebSocketClient : IWebSocketClient + { + internal IWebSocketConnection connection; + /// + /// LeanCluod .NET Realtime SDK 内置默认的 WebSocketClient + /// 开发者可以在初始化的时候指定自定义的 WebSocketClient + /// + public DefaultWebSocketClient() + { + + } + + public event Action OnClosed; + public event Action OnOpened; + public event Action OnMessage; + + public bool IsOpen + { + get + { + return connection != null && connection.IsOpen; + } + } + + public void Close() + { + if (connection != null) + { + connection.OnOpened -= Connection_OnOpened; + connection.OnMessage -= Connection_OnMessage; + connection.OnClosed -= Connection_OnClosed; + connection.OnError -= Connection_OnError; + try { + connection.Close(); + } catch (Exception e) { + AVRealtime.PrintLog(string.Format("close websocket error: {0}", e.Message)); + } + } + } + + public void Disconnect() { + connection?.Close(); + } + + public void Open(string url, string protocol = null) + { + // 在每次打开时,重新创建 WebSocket 对象 + connection = WebSocketFactory.Create(); + connection.OnOpened += Connection_OnOpened; + connection.OnMessage += Connection_OnMessage; + connection.OnClosed += Connection_OnClosed; + connection.OnError += Connection_OnError; + if (!string.IsNullOrEmpty(protocol)) + { + url = string.Format("{0}?subprotocol={1}", url, protocol); + } + connection.Open(url, protocol); + } + + private void Connection_OnOpened() + { + OnOpened?.Invoke(); + } + + private void Connection_OnMessage(string obj) + { + AVRealtime.PrintLog("websocket<=" + obj); + OnMessage?.Invoke(obj); + } + + private void Connection_OnClosed() + { + AVRealtime.PrintLog("PCL websocket closed without parameters.."); + OnClosed?.Invoke(0, "", ""); + } + + private void Connection_OnError(string obj) + { + AVRealtime.PrintLog($"PCL websocket error: {obj}"); + connection?.Close(); + } + + public void Send(string message) + { + if (connection != null) + { + if (this.IsOpen) + { + connection.Send(message); + } + else + { + var log = "Connection is NOT open when send message"; + AVRealtime.PrintLog(log); + connection?.Close(); + } + } + else { + AVRealtime.PrintLog("Connection is NULL"); + } + } + + public Task Connect(string url, string protocol = null) { + var tcs = new TaskCompletionSource(); + Action onOpen = null; + Action onClose = null; + Action onError = null; + onOpen = () => { + AVRealtime.PrintLog("PCL websocket opened"); + connection.OnOpened -= onOpen; + connection.OnClosed -= onClose; + connection.OnError -= onError; + // 注册事件 + connection.OnMessage += Connection_OnMessage; + connection.OnClosed += Connection_OnClosed; + connection.OnError += Connection_OnError; + tcs.SetResult(true); + }; + onClose = () => { + connection.OnOpened -= onOpen; + connection.OnClosed -= onClose; + connection.OnError -= onError; + tcs.SetException(new Exception("连接关闭")); + }; + onError = (err) => { + AVRealtime.PrintLog(string.Format("连接错误:{0}", err)); + connection.OnOpened -= onOpen; + connection.OnClosed -= onClose; + connection.OnError -= onError; + try { + connection.Close(); + } catch (Exception e) { + AVRealtime.PrintLog(string.Format("关闭错误的 WebSocket 异常:{0}", e.Message)); + } finally { + tcs.SetException(new Exception(string.Format("连接错误:{0}", err))); + } + }; + + // 在每次打开时,重新创建 WebSocket 对象 + connection = WebSocketFactory.Create(); + connection.OnOpened += onOpen; + connection.OnClosed += onClose; + connection.OnError += onError; + // + if (!string.IsNullOrEmpty(protocol)) { + url = string.Format("{0}?subprotocol={1}", url, protocol); + } + connection.Open(url, protocol); + return tcs.Task; + } + } +} diff --git a/RTM/Source/Internal/WebSocket/Unity/DefaultWebSocketClient.Unity.cs b/RTM/Source/Internal/WebSocket/Unity/DefaultWebSocketClient.Unity.cs new file mode 100644 index 0000000..c0769b4 --- /dev/null +++ b/RTM/Source/Internal/WebSocket/Unity/DefaultWebSocketClient.Unity.cs @@ -0,0 +1,149 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using WebSocketSharp; +using System.Threading.Tasks; + +namespace LeanCloud.Realtime.Internal +{ + + /// + /// LeanCluod Unity Realtime SDK 内置默认的 WebSocketClient + /// 开发者可以在初始化的时候指定自定义的 WebSocketClient + /// + public class DefaultWebSocketClient : IWebSocketClient + { + WebSocket ws; + public bool IsOpen + { + get + { + if (ws == null) { return false; } + return ws.IsAlive; + } + } + + public event Action OnClosed; + public event Action OnMessage + { + add + { + onMesssageCount++; + AVRealtime.PrintLog("DefaultWebSocketClient.OnMessage event add with " + onMesssageCount + " times"); + m_OnMessage += value; + + } + remove + { + onMesssageCount--; + AVRealtime.PrintLog("DefaultWebSocketClient.OnMessage event remove with " + onMesssageCount + " times"); + m_OnMessage -= value; + } + } + private Action m_OnMessage; + private int onMesssageCount = 0; + public event Action OnOpened; + + public void Close() + { + ws.CloseAsync(); + ws.OnOpen -= OnOpen; + ws.OnMessage -= OnWebSokectMessage; + ws.OnClose -= OnClose; + } + + public void Disconnect() { + ws.CloseAsync(); + } + + public void Open(string url, string protocol = null) + { + if (!string.IsNullOrEmpty(protocol)) + { + url = string.Format("{0}?subprotocol={1}", url, protocol); + } + ws = new WebSocket(url); + ws.OnOpen += OnOpen; + ws.OnMessage += OnWebSokectMessage; + ws.OnClose += OnClose; + ws.ConnectAsync(); + } + + private void OnWebSokectMessage(object sender, MessageEventArgs e) + { + AVRealtime.PrintLog("websocket<=" + e.Data); + m_OnMessage?.Invoke(e.Data); + } + + private void OnClose(object sender, CloseEventArgs e) { + AVRealtime.PrintLog(string.Format("Unity websocket closed with {0}, {1}", e.Code, e.Reason)); + OnClosed?.Invoke(e.Code, e.Reason, null); + } + + void OnWebSocketError(object sender, ErrorEventArgs e) { + AVRealtime.PrintLog($"PCL websocket error: {e.Message}"); + ws?.Close(); + } + + private void OnOpen(object sender, EventArgs e) { + OnOpened?.Invoke(); + } + + public void Send(string message) + { + ws.SendAsync(message, (b) => + { + + }); + } + + public Task Connect(string url, string protocol = null) { + var tcs = new TaskCompletionSource(); + EventHandler onOpen = null; + EventHandler onClose = null; + EventHandler onError = null; + onOpen = (sender, e) => { + AVRealtime.PrintLog("PCL websocket opened"); + ws.OnOpen -= onOpen; + ws.OnClose -= onClose; + ws.OnError -= onError; + // 注册事件 + ws.OnMessage += OnWebSokectMessage; + ws.OnClose += OnClose; + ws.OnError += OnWebSocketError; + tcs.SetResult(true); + }; + onClose = (sender, e) => { + ws.OnOpen -= onOpen; + ws.OnClose -= onClose; + ws.OnError -= onError; + tcs.SetException(new Exception("连接关闭")); + }; + onError = (sender, e) => { + AVRealtime.PrintLog(string.Format("连接错误:{0}", e.Message)); + ws.OnOpen -= onOpen; + ws.OnClose -= onClose; + ws.OnError -= onError; + try { + ws.Close(); + } catch (Exception ex) { + AVRealtime.PrintLog(string.Format("关闭错误的 WebSocket 异常:{0}", ex.Message)); + } finally { + tcs.SetException(new Exception(string.Format("连接错误:{0}", e.Message))); + } + }; + + // 在每次打开时,重新创建 WebSocket 对象 + if (!string.IsNullOrEmpty(protocol)) { + url = string.Format("{0}?subprotocol={1}", url, protocol); + } + ws = new WebSocket(url); + ws.OnOpen += onOpen; + ws.OnClose += onClose; + ws.OnError += onError; + ws.ConnectAsync(); + return tcs.Task; + } + } +} diff --git a/RTM/Source/Internal/WebSocket/Unity/websocket-sharp.dll b/RTM/Source/Internal/WebSocket/Unity/websocket-sharp.dll new file mode 100644 index 0000000000000000000000000000000000000000..06740f7226730aa01542e3cd6fdedfcc619ad07f GIT binary patch literal 254464 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 j^5)itpDHFYgeS9@VN(D|>qpnrfh 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 + /// Audio message. + /// + [AVIMMessageClassName("_AVIMAudioMessage")] + [AVIMTypedMessageTypeInt(-3)] + public class AVIMAudioMessage : AVIMFileMessage + { + + } + + /// + /// Video message. + /// + [AVIMMessageClassName("_AVIMVideoMessage")] + [AVIMTypedMessageTypeInt(-4)] + public class AVIMVideoMessage: AVIMFileMessage + { + + } +} diff --git a/RTM/Source/Public/AVIMBinaryMessage.cs b/RTM/Source/Public/AVIMBinaryMessage.cs new file mode 100644 index 0000000..103d4b3 --- /dev/null +++ b/RTM/Source/Public/AVIMBinaryMessage.cs @@ -0,0 +1,42 @@ +using System; +using LeanCloud.Realtime.Internal; + +namespace LeanCloud.Realtime +{ + /// + /// 基于二进制数据的消息类型,可以直接发送 Byte 数组 + /// + [AVIMMessageClassName("_AVIMBinaryMessage")] + public class AVIMBinaryMessage : AVIMMessage + { + + /// + /// Initializes a new instance of the class. + /// + public AVIMBinaryMessage() + { + + } + /// + /// create new instance of AVIMBinnaryMessage + /// + /// + public AVIMBinaryMessage(byte[] data) + { + this.BinaryData = data; + } + + /// + /// Gets or sets the binary data. + /// + /// The binary data. + public byte[] BinaryData { get; set; } + + internal override MessageCommand BeforeSend(MessageCommand cmd) + { + var result = base.BeforeSend(cmd); + result = result.Binary(this.BinaryData); + return result; + } + } +} diff --git a/RTM/Source/Public/AVIMClient.cs b/RTM/Source/Public/AVIMClient.cs new file mode 100644 index 0000000..4715c07 --- /dev/null +++ b/RTM/Source/Public/AVIMClient.cs @@ -0,0 +1,1195 @@ +using LeanCloud; +using LeanCloud.Storage.Internal; +using LeanCloud.Realtime.Internal; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace LeanCloud.Realtime +{ + /// + /// 代表一个实时通信的终端用户 + /// + public class AVIMClient + { + private readonly string clientId; + private readonly AVRealtime _realtime; + internal readonly object mutex = new object(); + internal readonly object patchMutex = new object(); + + /// + /// 一些可变的配置选项,便于应对各种需求场景 + /// + public struct Configuration + { + /// + /// Gets or sets a value indicating whether this + /// auto read. + /// + /// true if auto read; otherwise, false. + public bool AutoRead { get; set; } + } + + /// + /// Gets or sets the current configuration. + /// + /// The current configuration. + public Configuration CurrentConfiguration + { + get; set; + } + + internal AVRealtime LinkedRealtime + { + get { return _realtime; } + } + + /// + /// 单点登录所使用的 Tag + /// + public string Tag + { + get; + private set; + } + + /// + /// 客户端的标识,在一个 Application 内唯一。 + /// + public string ClientId + { + get { return clientId; } + } + + //private EventHandler m_OnNoticeReceived; + ///// + ///// 接收到服务器的命令时触发的事件 + ///// + //public event EventHandler OnNoticeReceived + //{ + // add + // { + // m_OnNoticeReceived += value; + // } + // remove + // { + // m_OnNoticeReceived -= value; + // } + //} + + private int onMessageReceivedCount = 0; + private EventHandler m_OnMessageReceived; + /// + /// 接收到聊天消息的事件通知 + /// + public event EventHandler OnMessageReceived + { + add + { + onMessageReceivedCount++; + AVRealtime.PrintLog("AVIMClient.OnMessageReceived event add with " + onMessageReceivedCount + " times"); + m_OnMessageReceived += value; + } + remove + { + onMessageReceivedCount--; + AVRealtime.PrintLog("AVIMClient.OnMessageReceived event remove with" + onMessageReceivedCount + " times"); + m_OnMessageReceived -= value; + } + } + + /// + /// Occurs when on members joined. + /// + public event EventHandler OnMembersJoined; + + /// + /// Occurs when on members left. + /// + public event EventHandler OnMembersLeft; + + /// + /// Occurs when on kicked. + /// + public event EventHandler OnKicked; + + /// + /// Occurs when on invited. + /// + public event EventHandler OnInvited; + + internal event EventHandler OnOfflineMessageReceived; + + private EventHandler m_OnSessionClosed; + /// + /// 当前打开的链接被迫关闭时触发的事件回调 + /// 可能的原因有单点登录冲突,或者被 REST API 强制踢下线 + /// + public event EventHandler OnSessionClosed + { + add + { + m_OnSessionClosed += value; + } + remove + { + m_OnSessionClosed -= value; + } + } + + /// + /// 创建 AVIMClient 对象 + /// + /// + /// + internal AVIMClient(string clientId, AVRealtime realtime) + : this(clientId, null, realtime) + { + + } + + /// + /// + /// + /// + /// + /// + internal AVIMClient(string clientId, string tag, AVRealtime realtime) + { + this.clientId = clientId; + Tag = tag ?? tag; + _realtime = realtime; + + #region sdk 强制在接收到消息之后一定要向服务端回发 ack + var ackListener = new AVIMMessageListener(); + ackListener.OnMessageReceived += AckListener_OnMessageReceieved; + //this.RegisterListener(ackListener); + #endregion + + #region 默认要为当前 client 绑定一个消息的监听器,用作消息的事件通知 + var messageListener = new AVIMMessageListener(); + messageListener.OnMessageReceived += MessageListener_OnMessageReceived; + this.RegisterListener(messageListener); + #endregion + + #region 默认要为当前 client 绑定一个 session close 的监听器,用来监测单点登录冲突的事件通知 + var sessionListener = new SessionListener(); + sessionListener.OnSessionClosed += SessionListener_OnSessionClosed; + this.RegisterListener(sessionListener); + #endregion + + #region 默认要为当前 client 监听 Ta 所出的对话中的人员变动的被动消息通知 + var membersJoinedListener = new AVIMMembersJoinListener(); + membersJoinedListener.OnMembersJoined += MembersJoinedListener_OnMembersJoined; + this.RegisterListener(membersJoinedListener); + + var membersLeftListener = new AVIMMembersLeftListener(); + membersLeftListener.OnMembersLeft += MembersLeftListener_OnMembersLeft; + this.RegisterListener(membersLeftListener); + + var invitedListener = new AVIMInvitedListener(); + invitedListener.OnInvited += InvitedListener_OnInvited; + this.RegisterListener(invitedListener); + + var kickedListener = new AVIMKickedListener(); + kickedListener.OnKicked += KickedListener_OnKicked; + this.RegisterListener(kickedListener); + #endregion + + #region 当前 client id 离线的时间内,TA 所在的对话产生的普通消息会以离线消息的方式送达到 TA 下一次登录的客户端 + var offlineMessageListener = new OfflineMessageListener(); + offlineMessageListener.OnOfflineMessageReceived += OfflineMessageListener_OnOfflineMessageReceived; + this.RegisterListener(offlineMessageListener); + #endregion + + #region 当前 client 离线期间内产生的未读消息可以通过之后调用 Conversation.SyncStateAsync 获取一下离线期间内的未读状态 + var unreadListener = new ConversationUnreadListener(); + this.RegisterListener(unreadListener); + #endregion + + #region 消息补丁(修改或者撤回) + var messagePatchListener = new MessagePatchListener(); + messagePatchListener.OnReceived = (messages) => + { + foreach (var message in messages) { + if (message is AVIMRecalledMessage) { + m_OnMessageRecalled?.Invoke(this, new AVIMMessagePatchEventArgs(message)); + } else { + m_OnMessageUpdated?.Invoke(this, new AVIMMessagePatchEventArgs(message)); + } + } + }; + this.RegisterListener(messagePatchListener); + #endregion + + #region configuration + CurrentConfiguration = new Configuration() + { + AutoRead = true, + }; + #endregion + + } + + private void OfflineMessageListener_OnOfflineMessageReceived(object sender, AVIMMessageEventArgs e) + { + if (OnOfflineMessageReceived != null) + { + OnOfflineMessageReceived(this, e); + } + this.AckListener_OnMessageReceieved(sender, e); + } + + private void KickedListener_OnKicked(object sender, AVIMOnKickedEventArgs e) + { + if (OnKicked != null) + OnKicked(this, e); + } + + private void InvitedListener_OnInvited(object sender, AVIMOnInvitedEventArgs e) + { + if (OnInvited != null) + OnInvited(this, e); + } + + private void MembersLeftListener_OnMembersLeft(object sender, AVIMOnMembersLeftEventArgs e) + { + if (OnMembersLeft != null) + OnMembersLeft(this, e); + } + + private void MembersJoinedListener_OnMembersJoined(object sender, AVIMOnMembersJoinedEventArgs e) + { + if (OnMembersJoined != null) + OnMembersJoined(this, e); + } + + private void SessionListener_OnSessionClosed(int arg1, string arg2, string arg3) + { + if (m_OnSessionClosed != null) + { + var args = new AVIMSessionClosedEventArgs() + { + Code = arg1, + Reason = arg2, + Detail = arg3 + }; + if (args.Code == 4115 || args.Code == 4111) + { + this._realtime.sessionConflict = true; + } + + m_OnSessionClosed(this, args); + } + AVRealtime.PrintLog("SessionListener_OnSessionClosed invoked."); + //this.LinkedRealtime.LogOut(); + } + + private void MessageListener_OnMessageReceived(object sender, AVIMMessageEventArgs e) + { + if (this.m_OnMessageReceived != null) + { + this.m_OnMessageReceived.Invoke(this, e); + } + this.AckListener_OnMessageReceieved(sender, e); + } + + private void AckListener_OnMessageReceieved(object sender, AVIMMessageEventArgs e) + { + lock (mutex) + { + var ackCommand = new AckCommand().MessageId(e.Message.Id) + .ConversationId(e.Message.ConversationId); + + // 在 v.2 协议下,只要在线收到消息,就默认是已读的,下次上线不会再把当前消息当做未读消息 + if (this.LinkedRealtime.CurrentConfiguration.OfflineMessageStrategy == AVRealtime.OfflineMessageStrategy.UnreadNotice) + { + ackCommand = ackCommand.ReadAck(); + } + + this.RunCommandAsync(ackCommand); + } + } + + private void UpdateUnreadNotice(object sender, AVIMMessageEventArgs e) + { + ConversationUnreadListener.UpdateNotice(e.Message); + } + + #region listener + + /// + /// 注册 IAVIMListener + /// + /// + /// + public void RegisterListener(IAVIMListener listener, Func runtimeHook = null) + { + _realtime.SubscribeNoticeReceived(listener, runtimeHook); + } + + #region get client instance + /// + /// Get the specified clientId. + /// + /// The get. + /// Client identifier. + public static AVIMClient Get(string clientId) + { + if (AVRealtime.clients == null || !AVRealtime.clients.ContainsKey(clientId)) throw new Exception(string.Format("no client found with a id in {0}", clientId)); + + return AVRealtime.clients[clientId]; + } + #endregion + + #endregion + /// + /// 创建对话 + /// + /// 对话 + /// 是否创建唯一对话,当 isUnique 为 true 时,如果当前已经有相同成员的对话存在则返回该对话,否则会创建新的对话。该值默认为 false。 + /// + internal Task CreateConversationAsync(AVIMConversation conversation, bool isUnique = true) + { + var cmd = new ConversationCommand() + .Generate(conversation) + .Unique(isUnique); + + var convCmd = cmd.Option("start") + .PeerId(clientId); + + return LinkedRealtime.AttachSignature(convCmd, LinkedRealtime.SignatureFactory.CreateStartConversationSignature(this.clientId, conversation.MemberIds)).OnSuccess(_ => + { + return this.RunCommandAsync(convCmd).OnSuccess(t => + { + var result = t.Result; + if (result.Item1 < 1) + { + var members = conversation.MemberIds.ToList(); + members.Add(ClientId); + conversation.MemberIds = members; + conversation.MergeFromPushServer(result.Item2); + } + + return conversation; + }); + }).Unwrap(); + } + + /// + /// 创建与目标成员的对话. + /// + /// 返回对话实例. + /// 目标成员. + /// 目标成员列表. + /// 对话名称. + /// 是否是系统对话. + /// 是否为暂态对话(聊天室). + /// 是否是唯一对话. + /// 自定义属性. + public Task CreateConversationAsync(string member = null, + IEnumerable members = null, + string name = "", + bool isSystem = false, + bool isTransient = false, + bool isUnique = true, + bool isTemporary = false, + int ttl = 86400, + IDictionary options = null) + { + if (member == null) member = ClientId; + var membersAsList = Concat(member, members, "创建对话时被操作的 member(s) 不可以为空。"); + var conversation = new AVIMConversation(members: membersAsList, + name: name, + isUnique: isUnique, + isSystem: isSystem, + isTransient: isTransient, + isTemporary: isTemporary, + ttl: ttl, + client: this); + if (options != null) + { + foreach (var key in options.Keys) + { + conversation[key] = options[key]; + } + } + return CreateConversationAsync(conversation, isUnique); + } + + /// + /// Creates the conversation async. + /// + /// The conversation async. + /// Builder. + public Task CreateConversationAsync(IAVIMConversatioBuilder builder) + { + var conversation = builder.Build(); + return CreateConversationAsync(conversation, conversation.IsUnique); + } + + /// + /// Gets the conversatio builder. + /// + /// The conversatio builder. + public AVIMConversationBuilder GetConversationBuilder() + { + var builder = AVIMConversationBuilder.CreateDefaultBuilder(); + builder.Client = this; + return builder; + } + + /// + /// 创建虚拟对话,对话 id 是由本地直接生成,云端根据规则消息发送给指定的 client id(s) + /// + /// + /// + /// 过期时间,默认是一天(86400 秒),单位是秒 + /// + public Task CreateTemporaryConversationAsync(string member = null, + IEnumerable members = null, int ttl = 86400) + { + if (member == null) member = ClientId; + var membersAsList = Concat(member, members, "创建对话时被操作的 member(s) 不可以为空。"); + return CreateConversationAsync(member, membersAsList, isTemporary: true, ttl: ttl); + } + + /// + /// 创建聊天室(即:暂态对话) + /// + /// 聊天室名称 + /// + public Task CreateChatRoomAsync(string chatroomName) + { + return CreateConversationAsync(name: chatroomName, isTransient: true); + } + + /// + /// 获取一个对话 + /// + /// 对话的 ID + /// 从服务器获取 + /// + public Task GetConversationAsync(string id, bool noCache = true) + { + if (!noCache) return Task.FromResult(new AVIMConversation(this) { ConversationId = id }); + else + { + return this.GetQuery().WhereEqualTo("objectId", id).FirstAsync(); + } + } + + #region send message + /// + /// 向目标对话发送消息 + /// + /// 目标对话 + /// 消息体 + /// + public Task SendMessageAsync( + AVIMConversation conversation, + IAVIMMessage message) + { + return this.SendMessageAsync(conversation, message, new AVIMSendOptions() + { + Receipt = true, + Transient = false, + Priority = 1, + Will = false, + PushData = null, + }); + } + + /// + /// 向目标对话发送消息 + /// + /// 目标对话 + /// 消息体 + /// 消息的发送选项,包含了一些特殊的标记 + /// + public Task SendMessageAsync( + AVIMConversation conversation, + IAVIMMessage message, + AVIMSendOptions options) + { + if (this.LinkedRealtime.State != AVRealtime.Status.Online) throw new Exception("未能连接到服务器,无法发送消息。"); + + var messageBody = message.Serialize(); + + message.ConversationId = conversation.ConversationId; + message.FromClientId = this.ClientId; + + var cmd = new MessageCommand() + .Message(messageBody) + .ConvId(conversation.ConversationId) + .Receipt(options.Receipt) + .Transient(options.Transient) + .Priority(options.Priority) + .Will(options.Will) + .MentionAll(message.MentionAll); + + if (message is AVIMMessage) + { + cmd = ((AVIMMessage)message).BeforeSend(cmd); + } + + if (options.PushData != null) + { + cmd = cmd.PushData(options.PushData); + } + + if (message.MentionList != null) + { + cmd = cmd.Mention(message.MentionList); + } + + var directCmd = cmd.PeerId(this.ClientId); + + return this.RunCommandAsync(directCmd).OnSuccess(t => + { + var response = t.Result.Item2; + + message.Id = response["uid"].ToString(); + message.ServerTimestamp = long.Parse(response["t"].ToString()); + + return message; + + }); + } + + + #endregion + + #region mute & unmute + /// + /// 当前用户对目标对话进行静音操作 + /// + /// + /// + public Task MuteConversationAsync(AVIMConversation conversation) + { + var convCmd = new ConversationCommand() + .ConversationId(conversation.ConversationId) + .Option("mute") + .PeerId(this.ClientId); + + return this.RunCommandAsync(convCmd); + } + /// + /// 当前用户对目标对话取消静音,恢复该对话的离线消息推送 + /// + /// + /// + public Task UnmuteConversationAsync(AVIMConversation conversation) + { + var convCmd = new ConversationCommand() + .ConversationId(conversation.ConversationId) + .Option("unmute") + .PeerId(this.ClientId); + + return this.RunCommandAsync(convCmd); + } + #endregion + + #region Conversation members operations + internal Task OperateMembersAsync(AVIMConversation conversation, string action, string member = null, IEnumerable members = null) + { + if (string.IsNullOrEmpty(conversation.ConversationId)) + { + throw new Exception("conversation id 不可以为空。"); + } + + var membersAsList = Concat(member, members, "加人或者踢人的时候,被操作的 member(s) 不可以为空。"); + + var cmd = new ConversationCommand().ConversationId(conversation.ConversationId) + .Members(membersAsList) + .Option(action) + .PeerId(clientId); + + return this.LinkedRealtime.AttachSignature(cmd, LinkedRealtime.SignatureFactory.CreateConversationSignature(conversation.ConversationId, ClientId, membersAsList, ConversationSignatureAction.Add)).OnSuccess(_ => + { + return this.RunCommandAsync(cmd).OnSuccess(t => + { + var result = t.Result; + if (!conversation.IsTransient) + { + if (conversation.MemberIds == null) conversation.MemberIds = new List(); + conversation.MemberIds = conversation.MemberIds.Concat(membersAsList); + } + return result; + }); + }).Unwrap(); + } + internal IEnumerable Concat(T single, IEnumerable collection, string exString = null) + { + List asList = null; + if (collection == null) + { + collection = new List(); + } + asList = collection.ToList(); + if (asList.Count == 0 && single == null) + { + exString = exString ?? "can not cancat a collection with a null value."; + throw new ArgumentNullException(exString); + } + asList.Add(single); + return asList; + } + + #region Join + /// + /// 当前用户加入到目标的对话中 + /// + /// 目标对话 + /// + public Task JoinAsync(AVIMConversation conversation) + { + return this.OperateMembersAsync(conversation, "add", this.ClientId); + } + #endregion + + #region Invite + /// + /// 直接将其他人加入到目标对话 + /// 被操作的人会在客户端会触发 OnInvited 事件,而已经存在于对话的用户会触发 OnMembersJoined 事件 + /// + /// 目标对话 + /// 单个的 Client Id + /// Client Id 集合 + /// + public Task InviteAsync(AVIMConversation conversation, string member = null, IEnumerable members = null) + { + return this.OperateMembersAsync(conversation, "add", member, members); + } + #endregion + + #region Left + /// + /// 当前 Client 离开目标对话 + /// 可以理解为是 QQ 群的退群操作 + /// + /// + /// 目标对话 + /// + [Obsolete("use LeaveAsync instead.")] + public Task LeftAsync(AVIMConversation conversation) + { + return this.OperateMembersAsync(conversation, "remove", this.ClientId); + } + + /// + /// Leaves the conversation async. + /// + /// The async. + /// Conversation. + public Task LeaveAsync(AVIMConversation conversation) + { + return this.OperateMembersAsync(conversation, "remove", this.ClientId); + } + #endregion + + #region Kick + /// + /// 从目标对话中剔除成员 + /// + /// 目标对话 + /// 被剔除的单个成员 + /// 被剔除的成员列表 + /// + public Task KickAsync(AVIMConversation conversation, string member = null, IEnumerable members = null) + { + return this.OperateMembersAsync(conversation, "remove", member, members); + } + #endregion + + #endregion + + #region Query && Message history && ack + + /// + /// Get conversation query. + /// + /// + public AVIMConversationQuery GetQuery() + { + return GetConversationQuery(); + } + + /// + /// Get conversation query. + /// + /// The conversation query. + public AVIMConversationQuery GetConversationQuery() + { + return new AVIMConversationQuery(this); + } + + #region load message history + + /// + /// 查询目标对话的历史消息 + /// 不支持聊天室(暂态对话) + /// + /// 目标对话 + /// 从 beforeMessageId 开始向前查询(和 beforeTimeStampPoint 共同使用,为防止某毫秒时刻有重复消息) + /// 截止到某个 afterMessageId (不包含) + /// 从 beforeTimeStampPoint 开始向前查询 + /// 拉取截止到 afterTimeStampPoint 时间戳(不包含) + /// 查询方向,默认是 1,如果是 1 表示从新消息往旧消息方向, 0 则相反,其他值无效 + /// 拉取消息条数,默认值 20 条,可设置为 1 - 1000 之间的任意整数 + /// + public Task> QueryMessageAsync(AVIMConversation conversation, + string beforeMessageId = null, + string afterMessageId = null, + DateTime? beforeTimeStampPoint = null, + DateTime? afterTimeStampPoint = null, + int direction = 1, + int limit = 20) + where T : IAVIMMessage + { + var maxLimit = 1000; + var actualLimit = limit > maxLimit ? maxLimit : limit; + var logsCmd = new AVIMCommand() + .Command("logs") + .Argument("cid", conversation.ConversationId) + .Argument("l", actualLimit); + + if (beforeMessageId != null) + { + logsCmd = logsCmd.Argument("mid", beforeMessageId); + } + + if (afterMessageId != null) + { + logsCmd = logsCmd.Argument("tmid", afterMessageId); + } + + if (beforeTimeStampPoint != null && beforeTimeStampPoint.Value != DateTime.MinValue) + { + logsCmd = logsCmd.Argument("t", beforeTimeStampPoint.Value.ToUnixTimeStamp()); + } + + if (afterTimeStampPoint != null && afterTimeStampPoint.Value != DateTime.MinValue) + { + logsCmd = logsCmd.Argument("tt", afterTimeStampPoint.Value.ToUnixTimeStamp()); + } + + if (direction == 0) + { + logsCmd = logsCmd.Argument("direction", "NEW"); + } + + var subMessageType = typeof(T); + var subTypeInteger = subMessageType == typeof(AVIMTypedMessage) ? 0 : FreeStyleMessageClassInfo.GetTypedInteger(subMessageType.GetTypeInfo()); + + if (subTypeInteger != 0) + { + logsCmd = logsCmd.Argument("lctype", subTypeInteger); + } + + return this.RunCommandAsync(logsCmd).OnSuccess(t => + { + var rtn = new List(); + var result = t.Result.Item2; + var logs = result["logs"] as List; + if (logs != null) + { + foreach (var log in logs) + { + var logMap = log as IDictionary; + if (logMap != null) + { + var msgStr = logMap["data"].ToString(); + var messageObj = AVRealtime.FreeStyleMessageClassingController.Instantiate(msgStr, logMap); + messageObj.ConversationId = conversation.ConversationId; + rtn.Add(messageObj); + } + } + } + + conversation.OnMessageLoad(rtn); + + return rtn.AsEnumerable().OfType(); + }); + } + #endregion + + + //public Task MarkAsReadAsync(string conversationId = null, string messageId = null, AVIMConversation conversation = null, AVIMMessage message = null) + //{ + // var msgId = messageId != null ? messageId : message.Id; + // var convId = conversationId != null ? conversationId : conversation.ConversationId; + // if (convId == null && msgId == null) throw new ArgumentNullException("发送已读回执的时候,必须指定 conversation id 或者 message id"); + // lock (mutex) + // { + // var ackCommand = new AckCommand() + // .ReadAck().MessageId(msgId) + // .ConversationId(convId) + // .PeerId(this.ClientId); + + // return this.RunCommandAsync(ackCommand); + // } + //} + #region 查询对话中对方的接收状态,也就是已读回执 + private Task> FetchAllReceiptTimestampsAsync(string targetClientId = null, string conversationId = null, AVIMConversation conversation = null, bool queryAllMembers = false) + { + var convId = conversationId != null ? conversationId : conversation.ConversationId; + if (convId == null) throw new ArgumentNullException("conversationId 和 conversation 不可以同时为 null"); + + var cmd = new ConversationCommand().ConversationId(convId) + .TargetClientId(targetClientId) + .QueryAllMembers(queryAllMembers) + .Option("max-read") + .PeerId(clientId); + + return this.RunCommandAsync(cmd).OnSuccess(t => + { + var result = t.Result; + long maxReadTimestamp = -1; + long maxAckTimestamp = -1; + + if (result.Item2.ContainsKey("maxReadTimestamp")) + { + long.TryParse(result.Item2["maxReadTimestamp"].ToString(), out maxReadTimestamp); + } + if (result.Item2.ContainsKey("maxAckTimestamp")) + { + long.TryParse(result.Item2["maxAckTimestamp"].ToString(), out maxAckTimestamp); + } + return new Tuple(maxAckTimestamp, maxReadTimestamp); + + }); + } + #endregion + + #region 查询对方是否在线 + /// + /// 查询对方 client Id 是否在线 + /// + /// 单个 client Id + /// 多个 client Id 集合 + /// + public Task>> PingAsync(string targetClientId = null, IEnumerable targetClientIds = null) + { + List queryIds = null; + if (targetClientIds != null) queryIds = targetClientIds.ToList(); + if (queryIds == null && string.IsNullOrEmpty(targetClientId)) throw new ArgumentNullException("必须查询至少一个 client id 的状态,targetClientId 和 targetClientIds 不可以同时为空"); + queryIds.Add(targetClientId); + + var cmd = new SessionCommand() + .SessionPeerIds(queryIds) + .Option("query"); + + return this.RunCommandAsync(cmd).OnSuccess(t => + { + var result = t.Result; + List> rtn = new List>(); + var onlineSessionPeerIds = AVDecoder.Instance.DecodeList(result.Item2["onlineSessionPeerIds"]); + foreach (var peerId in targetClientIds) + { + rtn.Add(new Tuple(peerId, onlineSessionPeerIds.Contains(peerId))); + } + return rtn.AsEnumerable(); + }); + } + #endregion + #region 获取暂态对话在线人数 + /// + /// 获取暂态对话(聊天室)在线人数,依赖缓存,并不一定 100% 与真实数据一致。 + /// + /// + /// + public Task CountOnlineClientsAsync(string chatroomId) + { + var command = new AVCommand(relativeUri: "rtm/transient_group/onlines?gid=" + chatroomId, method: "GET", + sessionToken: null, + headers: null, + data: null); + + return AVPlugins.Instance.CommandRunner.RunCommandAsync(command).OnSuccess(t => + { + var result = t.Result.Item2; + if (result.ContainsKey("result")) + { + return int.Parse(result["result"].ToString()); + } + return -1; + }); + } + #endregion + #endregion + + #region mark as read + + /// + /// + /// + /// + /// + /// + /// + public Task ReadAsync(AVIMConversation conversation, IAVIMMessage message = null, DateTime? readAt = null) + { + var convRead = new ReadCommand.ConvRead() + { + ConvId = conversation.ConversationId, + }; + + if (message != null) + { + convRead.MessageId = message.Id; + convRead.Timestamp = message.ServerTimestamp; + } + + if (readAt != null && readAt.Value != DateTime.MinValue) + { + convRead.Timestamp = readAt.Value.ToUnixTimeStamp(); + } + + var readCmd = new ReadCommand().Conv(convRead).PeerId(this.ClientId); + + this.RunCommandAsync(readCmd); + + return Task.FromResult(true); + } + + /// + /// mark the conversation as read with conversation id. + /// + /// conversation id + /// + public Task ReadAsync(string conversationId) + { + var conv = AVIMConversation.CreateWithoutData(conversationId, this); + return this.ReadAsync(conv); + } + + /// + /// mark all conversations as read. + /// + /// + public Task ReadAllAsync() + { + var cids = ConversationUnreadListener.FindAllConvIds(); + var readCmd = new ReadCommand().ConvIds(cids).PeerId(this.ClientId); + return this.RunCommandAsync(readCmd); + } + #endregion + + #region recall & modify + + /// + /// Recalls the async. + /// + /// The async. + /// Message. + public Task RecallAsync(IAVIMMessage message) + { + var tcs = new TaskCompletionSource(); + var patchCmd = new PatchCommand().Recall(message); + RunCommandAsync(patchCmd) + .OnSuccess(t => { + var recalledMsg = new AVIMRecalledMessage(); + AVIMMessage.CopyMetaData(message, recalledMsg); + tcs.SetResult(recalledMsg); + }); + return tcs.Task; + } + + /// + /// Modifies the aysnc. + /// + /// The aysnc. + /// 要修改的消息对象 + /// 新的消息对象 + public Task UpdateAsync(IAVIMMessage oldMessage, IAVIMMessage newMessage) + { + var tcs = new TaskCompletionSource(); + var patchCmd = new PatchCommand().Modify(oldMessage, newMessage); + this.RunCommandAsync(patchCmd) + .OnSuccess(t => { + // 从旧消息对象中拷贝数据 + AVIMMessage.CopyMetaData(oldMessage, newMessage); + // 获取更新时间戳 + var response = t.Result.Item2; + if (response.TryGetValue("lastPatchTime", out object updatedAtObj) && + long.TryParse(updatedAtObj.ToString(), out long updatedAt)) { + newMessage.UpdatedAt = updatedAt; + } + tcs.SetResult(newMessage); + }); + return tcs.Task; + } + + internal EventHandler m_OnMessageRecalled; + /// + /// Occurs when on message recalled. + /// + public event EventHandler OnMessageRecalled + { + add + { + this.m_OnMessageRecalled += value; + } + remove + { + this.m_OnMessageRecalled -= value; + } + } + internal EventHandler m_OnMessageUpdated; + /// + /// Occurs when on message modified. + /// + public event EventHandler OnMessageUpdated + { + add + { + this.m_OnMessageUpdated += value; + } + remove + { + this.m_OnMessageUpdated -= value; + } + } + + #endregion + + #region log out + /// + /// 退出登录或者切换账号 + /// + /// + public Task CloseAsync() + { + var cmd = new SessionCommand().Option("close"); + return this.RunCommandAsync(cmd).ContinueWith(t => + { + m_OnSessionClosed(this, null); + }); + } + #endregion + + /// + /// Run command async. + /// + /// The command async. + /// Command. + public Task>> RunCommandAsync(AVIMCommand command) + { + command.PeerId(this.ClientId); + return this.LinkedRealtime.RunCommandAsync(command); + } + + /// + /// Run command. + /// + /// Command. + public void RunCommand(AVIMCommand command) + { + command.PeerId(this.ClientId); + this.LinkedRealtime.RunCommand(command); + } + } + + /// + /// AVIMClient extensions. + /// + public static class AVIMClientExtensions + { + /// + /// Create conversation async. + /// + /// The conversation async. + /// Client. + /// Members. + public static Task CreateConversationAsync(this AVIMClient client, IEnumerable members) + { + return client.CreateConversationAsync(members: members); + } + + public static Task CreateConversationAsync(this AVIMClient client, IEnumerable members, string conversationName) + { + return client.CreateConversationAsync(members: members, name: conversationName); + } + + /// + /// Get conversation. + /// + /// The conversation. + /// Client. + /// Conversation identifier. + public static AVIMConversation GetConversation(this AVIMClient client, string conversationId) + { + return AVIMConversation.CreateWithoutData(conversationId, client); + } + + /// + /// Join conversation async. + /// + /// The async. + /// Client. + /// Conversation identifier. + public static Task JoinAsync(this AVIMClient client, string conversationId) + { + var conversation = client.GetConversation(conversationId); + return client.JoinAsync(conversation); + } + + /// + /// Leave conversation async. + /// + /// The async. + /// Client. + /// Conversation identifier. + public static Task LeaveAsync(this AVIMClient client, string conversationId) + { + var conversation = client.GetConversation(conversationId); + return client.LeaveAsync(conversation); + } + + /// + /// Query messages. + /// + /// The message async. + /// Client. + /// Conversation. + /// Before message identifier. + /// After message identifier. + /// Before time stamp point. + /// After time stamp point. + /// Direction. + /// Limit. + public static Task> QueryMessageAsync(this AVIMClient client, + AVIMConversation conversation, + string beforeMessageId = null, + string afterMessageId = null, + DateTime? beforeTimeStampPoint = null, + DateTime? afterTimeStampPoint = null, + int direction = 1, + int limit = 20) + { + return client.QueryMessageAsync(conversation, + beforeMessageId, + afterMessageId, + beforeTimeStampPoint, + afterTimeStampPoint, + direction, + limit); + } + + /// + /// Get the chat room query. + /// + /// The chat room query. + /// Client. + public static AVIMConversationQuery GetChatRoomQuery(this AVIMClient client) + { + return client.GetQuery().WhereEqualTo("tr", true); + } + } +} diff --git a/RTM/Source/Public/AVIMConversation.cs b/RTM/Source/Public/AVIMConversation.cs new file mode 100644 index 0000000..1bdf1cf --- /dev/null +++ b/RTM/Source/Public/AVIMConversation.cs @@ -0,0 +1,1545 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using LeanCloud.Realtime.Internal; +using LeanCloud; +using LeanCloud.Storage.Internal; +using System.Collections; +using System.IO; + +namespace LeanCloud.Realtime +{ + /// + /// 对话 + /// + public class AVIMConversation : IEnumerable>, IAVObject + { + private DateTime? updatedAt; + + private DateTime? createdAt; + + private DateTime? lastMessageAt; + + internal DateTime? expiredAt; + + private string name; + + private AVObject convState; + + internal readonly Object mutex = new Object(); + //private readonly IDictionary estimatedData = new Dictionary(); + + internal AVIMClient _currentClient; + + IEnumerator> IEnumerable>.GetEnumerator() + { + lock (mutex) + { + return ((IEnumerable>)convState).GetEnumerator(); + } + } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + lock (mutex) + { + return ((IEnumerable>)convState).GetEnumerator(); + } + } + + virtual public object this[string key] + { + get + { + return convState[key]; + } + set + { + convState[key] = value; + } + } + public ICollection Keys + { + get + { + lock (mutex) + { + return convState.Keys; + } + } + } + public T Get(string key) + { + return this.convState.Get(key); + } + public bool ContainsKey(string key) + { + return this.convState.ContainsKey(key); + } + + internal IDictionary EncodeAttributes() + { + var currentOperations = convState.StartSave(); + var jsonToSave = AVObject.ToJSONObjectForSaving(currentOperations); + return jsonToSave; + } + + internal void MergeFromPushServer(IDictionary json) + { + if (json.Keys.Contains("cdate")) + { + createdAt = DateTime.Parse(json["cdate"].ToString()); + updatedAt = DateTime.Parse(json["cdate"].ToString()); + json.Remove("cdate"); + } + if (json.Keys.Contains("lm")) + { + var ts = long.Parse(json["lm"].ToString()); + updatedAt = ts.ToDateTime(); + lastMessageAt = ts.ToDateTime(); + json.Remove("lm"); + } + if (json.Keys.Contains("c")) + { + Creator = json["c"].ToString(); + json.Remove("c"); + } + if (json.Keys.Contains("m")) + { + MemberIds = json["m"] as IList; + json.Remove("m"); + } + if (json.Keys.Contains("mu")) + { + MuteMemberIds = json["mu"] as IList; + json.Remove("mu"); + } + if (json.Keys.Contains("tr")) + { + IsTransient = bool.Parse(json["tr"].ToString()); + json.Remove("tr"); + } + if (json.Keys.Contains("sys")) + { + IsSystem = bool.Parse(json["sys"].ToString()); + json.Remove("sys"); + } + if (json.Keys.Contains("cid")) + { + ConversationId = json["cid"].ToString(); + json.Remove("cid"); + } + + if (json.Keys.Contains("name")) + { + Name = json["name"].ToString(); + json.Remove("name"); + } + } + + /// + /// 当前的AVIMClient,一个对话理论上只存在一个AVIMClient。 + /// + public AVIMClient CurrentClient + { + get + { + if (_currentClient == null) throw new NullReferenceException("当前对话没有关联有效的 AVIMClient。"); + return _currentClient; + } + //set + //{ + // _currentClient = value; + //} + } + /// + /// 对话的唯一ID + /// + public string ConversationId { get; internal set; } + + /// + /// 对话在全局的唯一的名字 + /// + public string Name + { + get + { + if (convState.ContainsKey("name")) + { + name = this.convState.Get("name"); + } + return name; + } + set + { + if (value == null) + this["name"] = ""; + else + { + this["name"] = value; + } + } + } + + /// + /// 对话中存在的 Client 的 ClientId 列表 + /// + public IEnumerable MemberIds { get; internal set; } + + /// + /// 对该对话静音的成员列表 + /// + /// 对该对话设置了静音的成员,将不会收到离线消息的推送。 + /// + /// + public IEnumerable MuteMemberIds { get; internal set; } + + /// + /// 对话的创建者 + /// + public string Creator { get; private set; } + + /// + /// 是否为聊天室 + /// + public bool IsTransient { get; internal set; } + + /// + /// 是否系统对话 + /// + public bool IsSystem { get; internal set; } + + /// + /// 是否是唯一对话 + /// + public bool IsUnique { get; internal set; } + + /// + /// 对话是否为虚拟对话 + /// + public bool IsTemporary { get; internal set; } + + /// + /// 对话创建的时间 + /// + public DateTime? CreatedAt + { + get + { + DateTime? nullable; + lock (this.mutex) + { + nullable = this.createdAt; + } + return nullable; + } + private set + { + lock (this.mutex) + { + this.createdAt = value; + } + } + } + + /// + /// 对话更新的时间 + /// + public DateTime? UpdatedAt + { + get + { + DateTime? nullable; + lock (this.mutex) + { + nullable = this.updatedAt; + } + return nullable; + } + private set + { + lock (this.mutex) + { + this.updatedAt = value; + } + } + } + + /// + /// 对话中最后一条消息的时间,可以用此判断对话的最后活跃时间 + /// + public DateTime? LastMessageAt + { + get + { + DateTime? nullable; + lock (this.mutex) + { + nullable = this.lastMessageAt; + } + return nullable; + } + private set + { + lock (this.mutex) + { + this.lastMessageAt = value; + } + } + } + + /// + /// 已知 id,在本地构建一个 AVIMConversation 对象 + /// + public AVIMConversation(string id) + : this(id, null) + { + + } + + /// + /// 已知 id 在本地构建一个对话 + /// + /// 对话 id + /// AVIMClient 实例,必须是登陆成功的 + public AVIMConversation(string id, AVIMClient client) : this(client) + { + this.ConversationId = id; + } + + internal AVIMConversation(AVIMClient client) + { + this._currentClient = client; + this.CurrentClient.OnMessageReceived += CurrentClient_OnMessageReceived; + } + + /// + /// AVIMConversation Build 驱动器 + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + internal AVIMConversation(AVIMConversation source = null, + string name = null, + string creator = null, + IEnumerable members = null, + IEnumerable muteMembers = null, + bool isTransient = false, + bool isSystem = false, + IEnumerable> attributes = null, + AVObject state = null, + bool isUnique = true, + bool isTemporary = false, + int ttl = 86400, + AVIMClient client = null) : + this(client) + { + convState = source != null ? source.convState : new AVObject("_Conversation"); + + + this.Name = source?.Name; + this.MemberIds = source?.MemberIds; + this.Creator = source?.Creator; + this.MuteMemberIds = source?.MuteMemberIds; + + if (!string.IsNullOrEmpty(name)) + { + this.Name = name; + } + if (!string.IsNullOrEmpty(creator)) + { + this.Creator = creator; + } + if (members != null) + { + this.MemberIds = members.ToList(); + } + if (muteMembers != null) + { + this.MuteMemberIds = muteMembers.ToList(); + } + + this.IsTransient = isTransient; + this.IsSystem = isSystem; + this.IsUnique = isUnique; + this.IsTemporary = isTemporary; + this.expiredAt = DateTime.Now + new TimeSpan(0, 0, ttl); + + if (state != null) + { + convState = state; + this.ConversationId = state.ObjectId; + this.CreatedAt = state.CreatedAt; + this.UpdatedAt = state.UpdatedAt; + this.MergeMagicFields(state.ToDictionary(x => x.Key, x => x.Value)); + } + + if (attributes != null) + { + this.MergeMagicFields(attributes.ToDictionary(x => x.Key, x => x.Value)); + } + } + + /// + /// 从本地构建一个对话 + /// + /// 对话的 objectId + /// + /// + public static AVIMConversation CreateWithoutData(string convId, AVIMClient client) + { + return new AVIMConversation(client) + { + ConversationId = convId, + }; + } + + /// + /// + /// + /// + /// + /// + public static AVIMConversation CreateWithData(IEnumerable> magicFields, AVIMClient client) + { + if (magicFields is AVObject) + { + return new AVIMConversation(state: (AVObject)magicFields, client: client); + } + return new AVIMConversation(attributes: magicFields, client: client); + } + + #region save to cloud + /// + /// 将修改保存到云端 + /// + /// + public Task SaveAsync() + { + var cmd = new ConversationCommand() + .Generate(this); + + var convCmd = cmd.Option("update") + .PeerId(this.CurrentClient.ClientId); + + return this.CurrentClient.RunCommandAsync(convCmd); + } + #endregion + + #region send message + /// + /// 向该对话发送消息。 + /// + /// 消息体 + /// 是否需要送达回执 + /// 是否是暂态消息,暂态消息不返回送达回执(ack),不保留离线消息,不触发离线推送 + /// 消息等级,默认是1,可选值还有 2 ,3 + /// 标记该消息是否为下线通知消息 + /// 如果消息的接收者已经下线了,这个字段的内容就会被离线推送到接收者 + /// 例如,一张图片消息的离线消息内容可以类似于:[您收到一条图片消息,点击查看] 这样的推送内容,参照微信的做法 + /// + /// + public Task SendMessageAsync(IAVIMMessage avMessage, + bool receipt = true, + bool transient = false, + int priority = 1, + bool will = false, + IDictionary pushData = null) + { + return this.SendMessageAsync(avMessage, new AVIMSendOptions() + { + Receipt = receipt, + Transient = transient, + Priority = priority, + Will = will, + PushData = pushData + }); + } + + /// + /// 发送消息 + /// + /// 消息体 + /// 消息的发送选项,包含了一些特殊的标记 + /// + public Task SendMessageAsync(IAVIMMessage avMessage, AVIMSendOptions options) + { + if (this.CurrentClient == null) throw new Exception("当前对话未指定有效 AVIMClient,无法发送消息。"); + return this.CurrentClient.SendMessageAsync(this, avMessage, options); + } + #endregion + + #region recall message + + #endregion + + #region message listener and event notify + + /// + /// Registers the listener. + /// + /// Listener. + public void RegisterListener(IAVIMListener listener) + { + this.CurrentClient.RegisterListener(listener, this.ConversationIdHook); + } + + internal bool ConversationIdHook(AVIMNotice notice) + { + if (!notice.RawData.ContainsKey("cid")) return false; + return notice.RawData["cid"].ToString() == this.ConversationId; + } + #endregion + + #region mute && unmute + /// + /// 当前用户针对对话做静音操作 + /// + /// + public Task MuteAsync() + { + return this.CurrentClient.MuteConversationAsync(this); + } + /// + /// 当前用户取消对话的静音,恢复该对话的离线消息推送 + /// + /// + public Task UnmuteAsync() + { + return this.CurrentClient.UnmuteConversationAsync(this); + } + #endregion + + #region 成员操作相关接口 + + /// + /// Joins the async. + /// + /// The async. + public Task JoinAsync() + { + return AddMembersAsync(CurrentClient.ClientId); + } + + + /// + /// Adds the members async. + /// + /// The members async. + /// Client identifier. + /// Client identifiers. + public Task AddMembersAsync(string clientId = null, IEnumerable clientIds = null) + { + return this.CurrentClient.InviteAsync(this, clientId, clientIds); + } + + /// + /// Removes the members async. + /// + /// The members async. + /// Client identifier. + /// Client identifiers. + public Task RemoveMembersAsync(string clientId = null, IEnumerable clientIds = null) + { + return this.CurrentClient.KickAsync(this, clientId, clientIds); + } + + /// + /// Quits the async. + /// + /// The async. + public Task QuitAsync() + { + return RemoveMembersAsync(CurrentClient.ClientId); + } + #endregion + + #region load message history + /// + /// 获取当前对话的历史消息 + /// 不支持聊天室(暂态对话) + /// + /// 从 beforeMessageId 开始向前查询(和 beforeTimeStampPoint 共同使用,为防止某毫秒时刻有重复消息) + /// 截止到某个 afterMessageId (不包含) + /// 从 beforeTimeStampPoint 开始向前查询 + /// 拉取截止到 afterTimeStampPoint 时间戳(不包含) + /// 查询方向,默认是 1,如果是 1 表示从新消息往旧消息方向, 0 则相反,其他值无效 + /// 获取的消息数量 + /// + public Task> QueryMessageAsync( + string beforeMessageId = null, + string afterMessageId = null, + DateTime? beforeTimeStampPoint = null, + DateTime? afterTimeStampPoint = null, + int direction = 1, + int limit = 20) + where T : IAVIMMessage + { + return this.CurrentClient.QueryMessageAsync(this, beforeMessageId, afterMessageId, beforeTimeStampPoint, afterTimeStampPoint, direction, limit) + .OnSuccess(t => + { + //OnMessageLoad(t.Result); + return t.Result; + }); + } + + /// + /// Gets the message query. + /// + /// The message query. + public AVIMMessageQuery GetMessageQuery() + { + return new AVIMMessageQuery(this); + } + + /// + /// Gets the message pager. + /// + /// The message pager. + public AVIMMessagePager GetMessagePager() + { + return new AVIMMessagePager(this); + } + + #endregion + + #region 字典与对象之间的转换 + internal virtual void MergeMagicFields(IDictionary data) + { + lock (this.mutex) + { + if (data.ContainsKey("objectId")) + { + this.ConversationId = (data["objectId"] as String); + data.Remove("objectId"); + } + if (data.ContainsKey("createdAt")) + { + this.CreatedAt = AVDecoder.ParseDate(data["createdAt"] as string); + data.Remove("createdAt"); + } + if (data.ContainsKey("updatedAt")) + { + this.updatedAt = AVDecoder.ParseDate(data["updatedAt"] as string); + data.Remove("updatedAt"); + } + if (data.ContainsKey("name")) + { + this.Name = (data["name"] as String); + data.Remove("name"); + } + if (data.ContainsKey("lm")) + { + this.LastMessageAt = AVDecoder.Instance.Decode(data["lm"]) as DateTime?; + data.Remove("lm"); + } + if (data.ContainsKey("m")) + { + this.MemberIds = AVDecoder.Instance.DecodeList(data["m"]); + data.Remove("m"); + } + if (data.ContainsKey("mu")) + { + this.MuteMemberIds = AVDecoder.Instance.DecodeList(data["mu"]); + data.Remove("mu"); + } + if (data.ContainsKey("c")) + { + this.Creator = data["c"].ToString(); + data.Remove("c"); + } + if (data.ContainsKey("unique")) + { + if (data["unique"] is bool) + { + this.IsUnique = (bool)data["unique"]; + } + data.Remove("unique"); + } + foreach (var kv in data) + { + this[kv.Key] = kv.Value; + } + } + } + #endregion + + #region SyncStateAsync & unread & mark as read + /// + /// sync state from server.suhc unread state .etc; + /// + /// + public Task SyncStateAsync() + { + lock (mutex) + { + var rtn = new AggregatedState(); + rtn.Unread = GetUnreadStateFromLocal(); + return Task.FromResult(rtn); + } + } + + private UnreadState _unread; + private UnreadState _lastUnreadWhenOpenSession; + public UnreadState Unread + { + get + { + _lastUnreadWhenOpenSession = GetUnreadStateFromLocal(); + + // v.2 协议,只给出上次离线之后的未读消息,本次在线的收到的消息均视为已读 + if (this.CurrentClient.LinkedRealtime.CurrentConfiguration.OfflineMessageStrategy == AVRealtime.OfflineMessageStrategy.UnreadNotice) + { + _unread = _lastUnreadWhenOpenSession; + } + else if (this.CurrentClient.LinkedRealtime.CurrentConfiguration.OfflineMessageStrategy == AVRealtime.OfflineMessageStrategy.UnreadAck) + { + if (_lastUnreadWhenOpenSession == null) _unread = new UnreadState().MergeReceived(this.Received); + else _unread = _lastUnreadWhenOpenSession.MergeReceived(this.Received); + } + + return _unread; + } + + internal set + { + _unread = value; + } + } + + private readonly object receivedMutex = new object(); + public ReceivedState Received + { + get; set; + } + public ReadState Read + { + get; set; + } + + UnreadState GetUnreadStateFromLocal() + { + lock (mutex) + { + var notice = ConversationUnreadListener.Get(this.ConversationId); + if (notice != null) + { + var unreadState = new UnreadState() + { + LastMessage = notice.LastUnreadMessage, + SyncdAt = ConversationUnreadListener.NotifTime, + Count = notice.UnreadCount + }; + return unreadState; + } + + return null; + } + } + + internal void OnMessageLoad(IEnumerable messages) + { + var lastestInCollection = messages.OrderByDescending(m => m.ServerTimestamp).FirstOrDefault(); + if (lastestInCollection != null) + { + if (CurrentClient.CurrentConfiguration.AutoRead) + { + this.ReadAsync(lastestInCollection); + } + } + } + + /// + /// mark this conversation as read + /// + /// + public Task ReadAsync(IAVIMMessage message = null, DateTime? readAt = null) + { + // 标记已读必须至少是从上一次离线产生的最后一条消息开始,否则无法计算 Count + if (_lastUnreadWhenOpenSession != null) + { + if (_lastUnreadWhenOpenSession.LastMessage != null) + { + message = _lastUnreadWhenOpenSession.LastMessage; + } + } + return this.CurrentClient.ReadAsync(this, message, readAt).OnSuccess(t => + { + Received = null; + _lastUnreadWhenOpenSession = null; + Read = new ReadState() + { + ReadAt = readAt != null ? readAt.Value.ToUnixTimeStamp() : 0, + LastMessage = message, + SyncdAt = DateTime.Now.ToUnixTimeStamp() + }; + + }); + } + + /// + /// aggregated state for the conversation + /// + public class AggregatedState + { + /// + /// Unread state + /// + public UnreadState Unread { get; internal set; } + } + + /// + /// UnreadState recoder for the conversation + /// + public class UnreadState + { + /// + /// unread count + /// + public int Count { get; internal set; } + /// + /// last unread message + /// + public IAVIMMessage LastMessage { get; internal set; } + + /// + /// last sync timestamp + /// + public long SyncdAt { get; internal set; } + + internal UnreadState MergeReceived(ReceivedState receivedState) + { + if (receivedState == null) return this; + var count = Count + receivedState.Count; + var lastMessage = this.LastMessage; + if (receivedState.LastMessage != null) + { + lastMessage = receivedState.LastMessage; + } + var syncdAt = this.SyncdAt > receivedState.SyncdAt ? this.SyncdAt : receivedState.SyncdAt; + return new UnreadState() + { + Count = count, + LastMessage = lastMessage, + SyncdAt = syncdAt + }; + } + } + + public class ReceivedState + { + public int Count { get; internal set; } + /// + /// last received message + /// + public IAVIMMessage LastMessage { get; internal set; } + + /// + /// last sync timestamp + /// + public long SyncdAt { get; internal set; } + } + + public class ReadState + { + public long ReadAt { get; set; } + public IAVIMMessage LastMessage { get; internal set; } + public long SyncdAt { get; internal set; } + } + + #endregion + + #region on client message received to update unread + private void CurrentClient_OnMessageReceived(object sender, AVIMMessageEventArgs e) + { + if (this.CurrentClient.CurrentConfiguration.AutoRead) + { + this.ReadAsync(e.Message); + return; + } + lock (receivedMutex) + { + if (this.Received == null) this.Received = new ReceivedState(); + this.Received.Count++; + this.Received.LastMessage = e.Message; + this.Received.SyncdAt = DateTime.Now.ToUnixTimeStamp(); + } + } + #endregion + } + + /// + /// AVIMConversation extensions. + /// + public static class AVIMConversationExtensions + { + + /// + /// Send message async. + /// + /// The async. + /// Conversation. + /// Message. + /// Options. + public static Task SendAsync(this AVIMConversation conversation, IAVIMMessage message, AVIMSendOptions options) + { + return conversation.SendMessageAsync(message, options); + } + + /// + /// Send message async. + /// + /// The async. + /// Conversation. + /// Message. + /// Options. + /// The 1st type parameter. + public static Task SendAsync(this AVIMConversation conversation, T message, AVIMSendOptions options) + where T : IAVIMMessage + { + return conversation.SendMessageAsync(message, options).OnSuccess(t => + { + return (T)t.Result; + }); + } + + /// + /// Send message async. + /// + /// The async. + /// Conversation. + /// Message. + /// The 1st type parameter. + public static Task SendAsync(this AVIMConversation conversation, T message) + where T : IAVIMMessage + { + return conversation.SendMessageAsync(message).OnSuccess(t => + { + return (T)t.Result; + }); + } + + /// + /// Send text message. + /// + /// The text async. + /// Conversation. + /// Text. + public static Task SendTextAsync(this AVIMConversation conversation, string text) + { + return conversation.SendAsync(new AVIMTextMessage(text)); + } + + #region Image messages + + /// + /// Send image message async. + /// + /// The image async. + /// Conversation. + /// URL. + /// Name. + /// Text title. + /// Custom attributes. + public static Task SendImageAsync(this AVIMConversation conversation, string url, string name = null, string textTitle = null, IDictionary customAttributes = null) + { + return conversation.SendFileMessageAsync(url, name, textTitle, customAttributes); + } + + /// + /// Send image message async. + /// + /// The image async. + /// Conversation. + /// File name. + /// Data. + /// MIME type. + /// Text title. + /// Meta data. + /// Custom attributes. + public static Task SendImageAsync(this AVIMConversation conversation, string fileName, Stream data, string mimeType = null, string textTitle = null, IDictionary metaData = null, IDictionary customAttributes = null) + { + return conversation.SendFileMessageAsync(fileName, data, mimeType, textTitle, metaData, customAttributes); + } + + /// + /// Send image message async. + /// + /// The image async. + /// Conversation. + /// File name. + /// Data. + /// MIME type. + /// Text title. + /// Meta data. + /// Custom attributes. + public static Task SendImageAsync(this AVIMConversation conversation, string fileName, byte[] data, string mimeType = null, string textTitle = null, IDictionary metaData = null, IDictionary customAttributes = null) + { + return conversation.SendFileMessageAsync(fileName, data, mimeType, textTitle, metaData, customAttributes); + } + #endregion + + #region audio message + + /// + /// Send audio message async. + /// + /// The audio async. + /// Conversation. + /// URL. + /// Name. + /// Text title. + /// Custom attributes. + public static Task SendAudioAsync(this AVIMConversation conversation, string url, string name = null, string textTitle = null, IDictionary customAttributes = null) + { + return conversation.SendFileMessageAsync(name, url, textTitle, customAttributes); + } + + /// + /// Send audio message async. + /// + /// The audio async. + /// Conversation. + /// File name. + /// Data. + /// MIME type. + /// Text title. + /// Meta data. + /// Custom attributes. + public static Task SendAudioAsync(this AVIMConversation conversation, string fileName, Stream data, string mimeType = null, string textTitle = null, IDictionary metaData = null, IDictionary customAttributes = null) + { + return conversation.SendFileMessageAsync(fileName, data, mimeType, textTitle, metaData, customAttributes); + } + + /// + /// Send audio message async. + /// + /// The audio async. + /// Conversation. + /// File name. + /// Data. + /// MIME type. + /// Text title. + /// Meta data. + /// Custom attributes. + public static Task SendAudioAsync(this AVIMConversation conversation, string fileName, byte[] data, string mimeType = null, string textTitle = null, IDictionary metaData = null, IDictionary customAttributes = null) + { + return conversation.SendFileMessageAsync(fileName, data, mimeType, textTitle, metaData, customAttributes); + } + #endregion + + #region video message + /// + /// Send video message async. + /// + /// The video async. + /// Conversation. + /// URL. + /// Name. + /// Text title. + /// Custom attributes. + public static Task SendVideoAsync(this AVIMConversation conversation, string url, string name = null, string textTitle = null, IDictionary customAttributes = null) + { + return conversation.SendFileMessageAsync(name, url, textTitle, customAttributes); + } + /// + /// Send video message async. + /// + /// The video async. + /// Conversation. + /// File name. + /// Data. + /// MIME type. + /// Text title. + /// Meta data. + /// Custom attributes. + public static Task SendVideoAsync(this AVIMConversation conversation, string fileName, Stream data, string mimeType = null, string textTitle = null, IDictionary metaData = null, IDictionary customAttributes = null) + { + return conversation.SendFileMessageAsync(fileName, data, mimeType, textTitle, metaData, customAttributes); + } + + /// + /// Send video message async. + /// + /// The video async. + /// Conversation. + /// File name. + /// Data. + /// MIME type. + /// Text title. + /// Meta data. + /// Custom attributes. + public static Task SendVideoAsync(this AVIMConversation conversation, string fileName, byte[] data, string mimeType = null, string textTitle = null, IDictionary metaData = null, IDictionary customAttributes = null) + { + return conversation.SendFileMessageAsync(fileName, data, mimeType, textTitle, metaData, customAttributes); + } + #endregion + + /// + /// Send file message async. + /// + /// The file message async. + /// Conversation. + /// URL. + /// Name. + /// Text title. + /// Custom attributes. + /// The 1st type parameter. + public static Task SendFileMessageAsync(this AVIMConversation conversation, string url, string name = null, string textTitle = null, IDictionary customAttributes = null) + where T : AVIMFileMessage, new() + { + var fileMessage = AVIMFileMessage.FromUrl(name, url, textTitle, customAttributes); + return conversation.SendAsync(fileMessage); + } + + /// + /// Send file message async. + /// + /// The file message async. + /// Conversation. + /// File name. + /// Data. + /// MIME type. + /// Text title. + /// Meta data. + /// Custom attributes. + /// The 1st type parameter. + public static Task SendFileMessageAsync(this AVIMConversation conversation, string fileName, Stream data, string mimeType = null, string textTitle = null, IDictionary metaData = null, IDictionary customAttributes = null) + where T : AVIMFileMessage, new() + { + var fileMessage = AVIMFileMessage.FromStream(fileName, data, mimeType, textTitle, metaData, customAttributes); + + return fileMessage.File.SaveAsync().OnSuccess(fileUploaded => + { + return conversation.SendAsync(fileMessage); + }).Unwrap(); + } + + /// + /// Send file message async. + /// + /// The file message async. + /// Conversation. + /// File name. + /// Data. + /// MIME type. + /// Text title. + /// Meta data. + /// Custom attributes. + /// The 1st type parameter. + public static Task SendFileMessageAsync(this AVIMConversation conversation, string fileName, byte[] data, string mimeType = null, string textTitle = null, IDictionary metaData = null, IDictionary customAttributes = null) + where T : AVIMFileMessage, new() + { + var fileMessage = AVIMFileMessage.FromStream(fileName, new MemoryStream(data), mimeType, textTitle, metaData, customAttributes); + + return conversation.SendFileMessageAsync(fileName, new MemoryStream(data), mimeType, textTitle, metaData, customAttributes); + } + + /// + /// Send location async. + /// + /// The location async. + /// Conversation. + /// Point. + public static Task SendLocationAsync(this AVIMConversation conversation, AVGeoPoint point) + { + var locationMessage = new AVIMLocationMessage(point); + return conversation.SendAsync(locationMessage); + } + + /// + /// Query the message async. + /// + /// The message async. + /// Conversation. + /// Before message identifier. + /// After message identifier. + /// Before time stamp. + /// After time stamp. + /// Direction. + /// Limit. + public static Task> QueryMessageAsync( + this AVIMConversation conversation, + string beforeMessageId = null, + string afterMessageId = null, + long? beforeTimeStamp = null, + long? afterTimeStamp = null, + int direction = 1, + int limit = 20) + { + + return conversation.QueryMessageAsync(beforeMessageId, + afterMessageId, + beforeTimeStamp, + afterTimeStamp, + direction, + limit); + } + + /// + /// Query message with speciafic subclassing type. + /// + /// The message async. + /// Conversation. + /// Before message identifier. + /// After message identifier. + /// Before time stamp. + /// After time stamp. + /// Direction. + /// Limit. + /// The 1st type parameter. + public static Task> QueryMessageAsync( + this AVIMConversation conversation, + string beforeMessageId = null, + string afterMessageId = null, + long? beforeTimeStamp = null, + long? afterTimeStamp = null, + int direction = 1, + int limit = 20) + where T : IAVIMMessage + { + + return conversation.QueryMessageAsync(beforeMessageId, + afterMessageId, + beforeTimeStampPoint: beforeTimeStamp.HasValue ? beforeTimeStamp.Value.ToDateTime() : DateTime.MinValue, + afterTimeStampPoint: afterTimeStamp.HasValue ? afterTimeStamp.Value.ToDateTime() : DateTime.MinValue, + direction: direction, + limit: limit); + } + + /// + /// Query message record before the given message async. + /// + /// The message before async. + /// Conversation. + /// Message. + /// Limit. + public static Task> QueryMessageBeforeAsync( + this AVIMConversation conversation, + IAVIMMessage message, + int limit = 20) + { + return conversation.QueryMessageAsync(beforeMessageId: message.Id, beforeTimeStamp: message.ServerTimestamp, limit: limit); + } + + /// + /// Query message record after the given message async. + /// + /// The message after async. + /// Conversation. + /// Message. + /// Limit. + public static Task> QueryMessageAfterAsync( + this AVIMConversation conversation, + IAVIMMessage message, + int limit = 20) + { + return conversation.QueryMessageAsync(afterMessageId: message.Id, afterTimeStamp: message.ServerTimestamp, limit: limit); + } + + /// + /// Query messages after conversation created. + /// + /// The message after async. + /// Conversation. + /// Limit. + public static Task> QueryMessageFromOldToNewAsync( + this AVIMConversation conversation, + int limit = 20) + { + return conversation.QueryMessageAsync(afterTimeStamp: conversation.CreatedAt.Value.ToUnixTimeStamp(), limit: limit); + } + + + /// + /// Query messages in interval async. + /// + /// The message interval async. + /// Conversation. + /// Start. + /// End. + /// Limit. + public static Task> QueryMessageInIntervalAsync( + this AVIMConversation conversation, + IAVIMMessage start, + IAVIMMessage end, + int limit = 20) + { + return conversation.QueryMessageAsync( + beforeMessageId: end.Id, + beforeTimeStamp: end.ServerTimestamp, + afterMessageId: start.Id, + afterTimeStamp: start.ServerTimestamp, + limit: limit); + } + + /// + /// Recall message async. + /// + /// The async. + /// Conversation. + /// Message. + public static Task RecallAsync(this AVIMConversation conversation, IAVIMMessage message) + { + return conversation.CurrentClient.RecallAsync(message); + } + + /// + /// Modifiy message async. + /// + /// The async. + /// Conversation. + /// 要修改的消息对象 + /// 新的消息对象 + public static Task UpdateAsync(this AVIMConversation conversation, IAVIMMessage oldMessage, IAVIMMessage newMessage) + { + return conversation.CurrentClient.UpdateAsync(oldMessage, newMessage); + } + } + + /// + /// AVIMConversatio builder. + /// + public interface IAVIMConversatioBuilder + { + /// + /// Build this instance. + /// + /// The build. + AVIMConversation Build(); + } + + /// + /// AVIMConversation builder. + /// + public class AVIMConversationBuilder : IAVIMConversatioBuilder + { + /// + /// Gets or sets the client. + /// + /// The client. + public AVIMClient Client { get; internal set; } + private bool isUnique; + private Dictionary properties; + + private string name; + private bool isTransient; + private bool isSystem; + private List members; + + + internal static AVIMConversationBuilder CreateDefaultBuilder() + { + return new AVIMConversationBuilder(); + } + + /// + /// Build this instance. + /// + /// The build. + public AVIMConversation Build() + { + var result = new AVIMConversation( + members: members, + name: name, + isUnique: isUnique, + isSystem: isSystem, + isTransient: isTransient, + client: Client); + + if (properties != null) + { + foreach (var key in properties.Keys) + { + result[key] = properties[key]; + } + } + + return result; + } + + /// + /// Sets the unique. + /// + /// The unique. + /// If set to true toggle. + public AVIMConversationBuilder SetUnique(bool toggle = true) + { + this.isUnique = toggle; + return this; + } + + /// + /// Sets the system. + /// + /// The system. + /// If set to true toggle. + public AVIMConversationBuilder SetSystem(bool toggle = true) + { + this.isSystem = toggle; + return this; + } + + /// + /// Sets the transient. + /// + /// The transient. + /// If set to true toggle. + public AVIMConversationBuilder SetTransient(bool toggle = true) + { + this.isTransient = toggle; + return this; + } + + /// + /// Sets the name. + /// + /// The name. + /// Name. + public AVIMConversationBuilder SetName(string name) + { + this.name = name; + return this; + } + + /// + /// Sets the members. + /// + /// The members. + /// Member client identifiers. + public AVIMConversationBuilder SetMembers(IEnumerable memberClientIds) + { + this.members = memberClientIds.ToList(); + return this; + } + + /// + /// Adds the member. + /// + /// The member. + /// Member client identifier. + public AVIMConversationBuilder AddMember(string memberClientId) + { + return AddMembers(new string[] { memberClientId }); + } + + /// + /// Adds the members. + /// + /// The members. + /// Member client identifiers. + public AVIMConversationBuilder AddMembers(IEnumerable memberClientIds) + { + if (this.members == null) this.members = new List(); + this.members.AddRange(memberClientIds); + return this; + } + + /// + /// Sets the property. + /// + /// The property. + /// Key. + /// Value. + public AVIMConversationBuilder SetProperty(string key, object value) + { + if (this.properties == null) this.properties = new Dictionary(); + this.properties[key] = value; + return this; + } + } + + /// + /// AVIMM essage emitter builder. + /// + public class AVIMMessageEmitterBuilder + { + private AVIMConversation _conversation; + /// + /// Sets the conversation. + /// + /// The conversation. + /// Conversation. + public AVIMMessageEmitterBuilder SetConversation(AVIMConversation conversation) + { + _conversation = conversation; + return this; + } + /// + /// Gets the conversation. + /// + /// The conversation. + public AVIMConversation Conversation + { + get + { + return _conversation; + } + } + + private AVIMSendOptions _sendOptions; + /// + /// Gets the send options. + /// + /// The send options. + public AVIMSendOptions SendOptions + { + get + { + return _sendOptions; + } + } + /// + /// Sets the send options. + /// + /// The send options. + /// Send options. + public AVIMMessageEmitterBuilder SetSendOptions(AVIMSendOptions sendOptions) + { + _sendOptions = sendOptions; + return this; + } + + private IAVIMMessage _message; + /// + /// Gets the message. + /// + /// The message. + public IAVIMMessage Message + { + get + { + return _message; + } + } + /// + /// Sets the message. + /// + /// The message. + /// Message. + public AVIMMessageEmitterBuilder SetMessage(IAVIMMessage message) + { + _message = message; + return this; + } + + /// + /// Send async. + /// + /// The async. + public Task SendAsync() + { + return this.Conversation.SendAsync(this.Message, this.SendOptions); + } + } +} diff --git a/RTM/Source/Public/AVIMConversationQuery.cs b/RTM/Source/Public/AVIMConversationQuery.cs new file mode 100644 index 0000000..cbb0f5f --- /dev/null +++ b/RTM/Source/Public/AVIMConversationQuery.cs @@ -0,0 +1,181 @@ +using LeanCloud.Realtime.Internal; +using LeanCloud.Storage.Internal; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace LeanCloud.Realtime +{ + /// + /// 对话查询类 + /// + public class AVIMConversationQuery : AVQueryPair, IAVQuery + { + internal AVIMClient CurrentClient { get; set; } + internal AVIMConversationQuery(AVIMClient _currentClient) + : base() + { + CurrentClient = _currentClient; + } + + bool compact; + bool withLastMessageRefreshed; + + private AVIMConversationQuery(AVIMConversationQuery source, + IDictionary where = null, + IEnumerable replacementOrderBy = null, + IEnumerable thenBy = null, + int? skip = null, + int? limit = null, + IEnumerable includes = null, + IEnumerable selectedKeys = null, + String redirectClassNameForKey = null) + : base(source, where, replacementOrderBy, thenBy, skip, limit, includes, selectedKeys, redirectClassNameForKey) + { + + } + + /// + /// Creates the instance. + /// + /// The instance. + /// Where. + /// Replacement order by. + /// Then by. + /// Skip. + /// Limit. + /// Includes. + /// Selected keys. + /// Redirect class name for key. + public override AVIMConversationQuery CreateInstance( + IDictionary where = null, + IEnumerable replacementOrderBy = null, + IEnumerable thenBy = null, + int? skip = null, + int? limit = null, + IEnumerable includes = null, + IEnumerable selectedKeys = null, + String redirectClassNameForKey = null) + { + var rtn = new AVIMConversationQuery(this, where, replacementOrderBy, thenBy, skip, limit, includes); + rtn.CurrentClient = this.CurrentClient; + rtn.compact = this.compact; + rtn.withLastMessageRefreshed = this.withLastMessageRefreshed; + return rtn; + } + + /// + /// Withs the last message refreshed. + /// + /// The last message refreshed. + /// If set to true enabled. + public AVIMConversationQuery WithLastMessageRefreshed(bool enabled) + { + this.withLastMessageRefreshed = enabled; + return this; + } + + public AVIMConversationQuery Compact(bool enabled) + { + this.compact = enabled; + return this; + } + + + internal ConversationCommand GenerateQueryCommand() + { + var cmd = new ConversationCommand(); + + var queryParameters = this.BuildParameters(false); + if (queryParameters != null) + { + if (queryParameters.Keys.Contains("where")) + cmd.Where(queryParameters["where"]); + + if (queryParameters.Keys.Contains("skip")) + cmd.Skip(int.Parse(queryParameters["skip"].ToString())); + + if (queryParameters.Keys.Contains("limit")) + cmd.Limit(int.Parse(queryParameters["limit"].ToString())); + + if (queryParameters.Keys.Contains("sort")) + cmd.Sort(queryParameters["order"].ToString()); + } + + return cmd; + } + + public override Task CountAsync(CancellationToken cancellationToken) + { + var convCmd = this.GenerateQueryCommand(); + convCmd.Count(); + convCmd.Limit(0); + var cmd = convCmd.Option("query"); + return CurrentClient.RunCommandAsync(convCmd).OnSuccess(t => + { + var result = t.Result.Item2; + + if (result.ContainsKey("count")) + { + return int.Parse(result["count"].ToString()); + } + return 0; + }); + } + + + /// + /// 查找符合条件的对话 + /// + /// + /// + public override Task> FindAsync(CancellationToken cancellationToken) + { + var convCmd = this.GenerateQueryCommand().Option("query"); + return CurrentClient.RunCommandAsync(convCmd).OnSuccess(t => + { + var result = t.Result.Item2; + + IList rtn = new List(); + var conList = result["results"] as IList; + if (conList != null) + { + foreach (var c in conList) + { + var cData = c as IDictionary; + if (cData != null) + { + var con = AVIMConversation.CreateWithData(cData, CurrentClient); + rtn.Add(con); + } + } + } + return rtn.AsEnumerable(); + }); + } + + public override Task FirstAsync(CancellationToken cancellationToken) + { + return this.FirstOrDefaultAsync(); + } + + public override Task FirstOrDefaultAsync(CancellationToken cancellationToken) + { + var firstQuery = this.Limit(1); + return firstQuery.FindAsync().OnSuccess(t => + { + return t.Result.FirstOrDefault(); + }); + } + + public override Task GetAsync(string objectId, CancellationToken cancellationToken) + { + var idQuery = this.WhereEqualTo("objectId", objectId); + return idQuery.FirstAsync(); + } + } + +} diff --git a/RTM/Source/Public/AVIMEnumerator.cs b/RTM/Source/Public/AVIMEnumerator.cs new file mode 100644 index 0000000..d9368bc --- /dev/null +++ b/RTM/Source/Public/AVIMEnumerator.cs @@ -0,0 +1,248 @@ +using LeanCloud.Storage.Internal; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using LeanCloud.Realtime.Internal; + +namespace LeanCloud.Realtime +{ + + /// + /// AVIMM essage pager. + /// + public class AVIMMessagePager + { + /// + /// Gets the query. + /// + /// The query. + public AVIMMessageQuery Query { get; private set; } + + /// + /// Gets the size of the page. + /// + /// The size of the page. + public int PageSize + { + get + { + return Query.Limit; + } + + private set + { + Query.Limit = value; + } + } + + /// + /// Gets the current message identifier flag. + /// + /// The current message identifier flag. + public string CurrentMessageIdFlag + { + get + { + return Query.StartMessageId; + } + private set + { + Query.StartMessageId = value; + } + } + + /// + /// Gets the current date time flag. + /// + /// The current date time flag. + public DateTime CurrentDateTimeFlag + { + get + { + return Query.From; + } + private set + { + Query.From = value; + } + } + + internal AVIMMessagePager() + { + + } + + /// + /// Initializes a new instance of the class. + /// + /// Conversation. + public AVIMMessagePager(AVIMConversation conversation) + : this() + { + Query = conversation.GetMessageQuery(); + PageSize = 20; + CurrentDateTimeFlag = DateTime.Now; + } + + /// + /// Sets the size of the page. + /// + /// The page size. + /// Page size. + public AVIMMessagePager SetPageSize(int pageSize) + { + PageSize = pageSize; + return this; + } + + /// + /// Previouses the async. + /// + /// The async. + public Task> PreviousAsync() + { + return Query.FindAsync().OnSuccess(t => + { + var headerMessage = t.Result.FirstOrDefault(); + if (headerMessage != null) + { + CurrentMessageIdFlag = headerMessage.Id; + CurrentDateTimeFlag = headerMessage.ServerTimestamp.ToDateTime(); + } + return t.Result; + }); + } + + /// + /// from previous to lastest. + /// + /// + public Task> NextAsync() + { + return Query.ReverseFindAsync().OnSuccess(t => + { + var tailMessage = t.Result.LastOrDefault(); + if (tailMessage != null) + { + CurrentMessageIdFlag = tailMessage.Id; + CurrentDateTimeFlag = tailMessage.ServerTimestamp.ToDateTime(); + } + return t.Result; + }); + } + } + + /// + /// history message interator. + /// + public class AVIMMessageQuery + { + /// + /// Gets or sets the convsersation. + /// + /// The convsersation. + public AVIMConversation Convsersation { get; set; } + /// + /// Gets or sets the limit. + /// + /// The limit. + public int Limit { get; set; } + /// + /// Gets or sets from. + /// + /// From. + public DateTime From { get; set; } + /// + /// Gets or sets to. + /// + /// To. + public DateTime To { get; set; } + /// + /// Gets or sets the end message identifier. + /// + /// The end message identifier. + public string EndMessageId { get; set; } + /// + /// Gets or sets the start message identifier. + /// + /// The start message identifier. + public string StartMessageId { get; set; } + + + internal AVIMMessageQuery() + { + Limit = 20; + From = DateTime.Now; + } + + /// + /// Initializes a new instance of the class. + /// + /// Conversation. + public AVIMMessageQuery(AVIMConversation conversation) + : this() + { + Convsersation = conversation; + } + + /// + /// Sets the limit. + /// + /// The limit. + /// Limit. + public AVIMMessageQuery SetLimit(int limit) + { + Limit = limit; + return this; + } + + /// + /// from lastest to previous. + /// + /// + public Task> FindAsync() + { + return FindAsync(); + } + + /// + /// from lastest to previous. + /// + /// + public Task> ReverseFindAsync() + { + return ReverseFindAsync(); + } + + /// + /// Finds the async. + /// + /// The async. + /// set direction to reverse,it means query direct is from old to new. + /// The 1st type parameter. + public Task> FindAsync(bool reverse = false) + where T : IAVIMMessage + { + return Convsersation.QueryMessageAsync( + beforeTimeStampPoint: From, + afterTimeStampPoint: To, + limit: Limit, + afterMessageId: EndMessageId, + beforeMessageId: StartMessageId, + direction: reverse ? 0 : 1); + } + + /// + /// from previous to lastest. + /// + /// + public Task> ReverseFindAsync() + where T : IAVIMMessage + { + return FindAsync(true); + } + } +} diff --git a/RTM/Source/Public/AVIMEventArgs.cs b/RTM/Source/Public/AVIMEventArgs.cs new file mode 100644 index 0000000..2b91b23 --- /dev/null +++ b/RTM/Source/Public/AVIMEventArgs.cs @@ -0,0 +1,251 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace LeanCloud.Realtime +{ + /// + /// + /// + public class AVIMEventArgs : EventArgs + { + public AVIMEventArgs() + { + + } + public AVIMException.ErrorCode ErrorCode { get; internal set; } + /// + /// LeanCloud 服务端发往客户端消息通知 + /// + public string Message { get; set; } + } + + public class AVIMDisconnectEventArgs : EventArgs + { + public int Code { get; private set; } + + public string Reason { get; private set; } + + public string Detail { get; private set; } + + public AVIMDisconnectEventArgs() + { + + } + public AVIMDisconnectEventArgs(int _code, string _reason, string _detail) + { + this.Code = _code; + this.Reason = _reason; + this.Detail = _detail; + } + } + + /// + /// 开始重连之后触发正在重连的事件通知,提供给监听者的事件参数 + /// + public class AVIMReconnectingEventArgs : EventArgs + { + /// + /// 是否由 SDK 内部机制启动的自动重连 + /// + public bool IsAuto { get; set; } + + /// + /// 重连的 client Id + /// + public string ClientId { get; set; } + + /// + /// 重连时使用的 SessionToken + /// + public string SessionToken { get; set; } + } + + /// + /// 重连成功之后的事件回调 + /// + public class AVIMReconnectedEventArgs : EventArgs + { + /// + /// 是否由 SDK 内部机制启动的自动重连 + /// + public bool IsAuto { get; set; } + + /// + /// 重连的 client Id + /// + public string ClientId { get; set; } + + /// + /// 重连时使用的 SessionToken + /// + public string SessionToken { get; set; } + } + + /// + /// 重连失败之后的事件回调参数 + /// + public class AVIMReconnectFailedArgs : EventArgs + { + /// + /// 是否由 SDK 内部机制启动的自动重连 + /// + public bool IsAuto { get; set; } + + /// + /// 重连的 client Id + /// + public string ClientId { get; set; } + + /// + /// 重连时使用的 SessionToken + /// + public string SessionToken { get; set; } + + /// + /// 失败的原因 + /// 0. 客户端网络断开 + /// 1. sessionToken 错误或者失效,需要重新创建 client + /// + public int FailedCode { get; set; } + } + + /// + /// AVIMM essage event arguments. + /// + public class AVIMMessageEventArgs : EventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// I message. + public AVIMMessageEventArgs(IAVIMMessage iMessage) + { + Message = iMessage; + } + /// + /// Gets or sets the message. + /// + /// The message. + public IAVIMMessage Message { get; internal set; } + } + + /// + /// AVIMMessage event arguments. + /// + public class AVIMMessagePatchEventArgs : EventArgs + { + public AVIMMessagePatchEventArgs(IAVIMMessage message) + { + Message = message; + } + + /// + /// Gets or sets the message. + /// + /// The message. + public IAVIMMessage Message { get; internal set; } + } + + /// + /// AVIMT ext message event arguments. + /// + public class AVIMTextMessageEventArgs : EventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// Raw. + public AVIMTextMessageEventArgs(AVIMTextMessage raw) + { + TextMessage = raw; + } + /// + /// Gets or sets the text message. + /// + /// The text message. + public AVIMTextMessage TextMessage { get; internal set; } + } + + /// + /// 当对话中有人加入时,触发 时所携带的事件参数 + /// + public class AVIMOnMembersJoinedEventArgs : EventArgs + { + /// + /// 加入到对话的 Client Id(s) + /// + public IEnumerable JoinedMembers { get; internal set; } + + /// + /// 邀请的操作人 + /// + public string InvitedBy { get; internal set; } + + /// + /// 此次操作针对的对话 Id + /// + public string ConversationId { get; internal set; } + } + + /// + /// 当对话中有人加入时,触发 AVIMMembersJoinListener 时所携带的事件参数 + /// + public class AVIMOnMembersLeftEventArgs : EventArgs + { + /// + /// 离开对话的 Client Id(s) + /// + public IEnumerable LeftMembers { get; internal set; } + + /// + /// 踢出的操作人 + /// + public string KickedBy { get; internal set; } + + /// + /// 此次操作针对的对话 Id + /// + public string ConversationId { get; internal set; } + } + /// + /// 当前用户被邀请加入到对话 + /// + public class AVIMOnInvitedEventArgs : EventArgs + { + /// + /// 邀请的操作人 + /// + public string InvitedBy { get; internal set; } + + /// + /// 此次操作针对的对话 Id + /// + public string ConversationId { get; internal set; } + } + + /// + /// 当前用户被他人从对话中踢出 + /// + public class AVIMOnKickedEventArgs : EventArgs + { + /// + /// 踢出的操作人 + /// + public string KickedBy { get; internal set; } + + /// + /// 此次操作针对的对话 Id + /// + public string ConversationId { get; internal set; } + } + + public class AVIMSessionClosedEventArgs : EventArgs + { + public int Code { get; internal set; } + + public string Reason { get; internal set; } + + public string Detail { get; internal set; } + } +} diff --git a/RTM/Source/Public/AVIMException.cs b/RTM/Source/Public/AVIMException.cs new file mode 100644 index 0000000..f473746 --- /dev/null +++ b/RTM/Source/Public/AVIMException.cs @@ -0,0 +1,235 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LeanCloud.Realtime +{ + /// + /// 实时通信的异常 + /// + public class AVIMException : Exception + { + /// + /// 错误代码 + /// + public enum ErrorCode + { + /// + /// Error code indicating that an unknown error or an error unrelated to LeanCloud + /// occurred. + /// + OtherCause = -1, + + /// + /// 服务端错误 + /// + FromServer = 4000, + + + /// + /// websocket 连接非正常关闭,通常见于路由器配置对长连接限制的情况。SDK 会自动重连,无需人工干预。 + /// + UnknownError = 1006, + + /// + /// 应用不存在或应用禁用了实时通信服务 + /// + APP_NOT_AVAILABLE = 4100, + + + /// + /// 登录签名验证失败 + /// + SIGNATURE_FAILED = 4102, + + /// + /// Client ClientId 格式错误,超过 64 个字符。 + /// + INVALID_LOGIN = 4103, + + /// + /// Session 没有打开就发送消息,或执行其他操作。常见的错误场景是调用 open session 后直接发送消息,正确的用法是在 Session 打开的回调里执行。 + /// + SESSION_REQUIRED = 4105, + + /// + /// 读超时,服务器端长时间没有收到客户端的数据,切断连接。SDK 包装了心跳包的机制,出现此错误通常是网络问题。SDK 会自动重连,无需人工干预。 + /// + READ_TIMEOUT = 4107, + + /// + /// 登录超时,连接后长时间没有完成 session open。通常是登录被拒绝等原因,出现此问题可能是使用方式有误,可以 创建工单,由我们技术顾问来给出建议。 + /// + LOGIN_TIMEOUT = 4108, + + /// + /// 包过长。消息大小超过 5 KB,请缩短消息或者拆分消息。 + /// + FRAME_TOO_LONG = 4109, + + /// + /// 设置安全域名后,当前登录的域名与安全域名不符合。 + /// + INVALID_ORIGIN = 4110, + + + /// + /// 设置单设备登录后,客户端被其他设备挤下线。 + /// + SESSION_CONFLICT = 4111, + + /// + /// 应用容量超限,当天登录用户数已经超过应用设定的最大值。 + /// + APP_QUOTA_EXCEEDED = 4113, + + /// + /// 客户端发送的序列化数据服务器端无法解析。 + /// + UNPARSEABLE_RAW_MESSAGE = 4114, + + /// + /// 客户端被 REST API 管理接口强制下线。 + /// + KICKED_BY_APP = 4115, + + /// + /// 应用单位时间内发送消息量超过限制,消息被拒绝。 + /// + MESSAGE_SENT_QUOTA_EXCEEDED = 4116, + + /// + /// 服务器内部错误,如果反复出现请收集相关线索并 创建工单,我们会尽快解决。 + /// + INTERNAL_ERROR = 4200, + + /// + /// 通过 API 发送消息超时 + /// + SEND_MESSAGE_TIMEOUT = 4201, + + /// + /// 上游 API 调用异常,请查看报错信息了解错误详情 + /// + CONVERSATION_API_FAILED = 4301, + + /// + /// 对话相关操作签名错误 + /// + CONVERSATION_SIGNATURE_FAILED = 4302, + + /// + /// 发送消息,或邀请等操作对应的对话不存在。 + /// + CONVERSATION_NOT_FOUND = 4303, + + /// + /// 对话成员已满,不能再添加。 + /// + CONVERSATION_FULL = 4304, + + /// + /// 对话操作被应用的云引擎 Hook 拒绝 + /// + CONVERSATION_REJECTED_BY_APP = 4305, + + /// + /// 更新对话操作失败 + /// + CONVERSATION_UPDATE_FAILED = 4306, + + /// + /// 该对话为只读,不能更新或增删成员。 + /// + CONVERSATION_READ_ONLY = 4307, + + /// + /// 该对话禁止当前用户发送消息 + /// + CONVERSATION_NOT_ALLOWED = 4308, + + /// + /// 更新对话的请求被拒绝,当前用户不在对话中 + /// + CONVERSATION_UPDATE_REJECT = 4309, + + /// + /// 查询对话失败,常见于慢查询导致的超时或受其他慢查询导致的数据库响应慢 + /// + CONVERSATION_QUERY_FAILED = 4310, + + /// + /// 拉取对话消息记录失败,常见与超时的情况 + /// + CONVERSATION_LOG_FAILED = 4311, + + /// + /// 拉去对话消息记录被拒绝,当前用户不再对话中 + /// + CONVERSATION_LOG_REJECT = 4312, + + /// + /// 该功能仅对系统对话有效 + /// + SYSTEM_CONVERSATION_REQUIRED = 4313, + + + /// + /// 该功能仅对普通对话有效。 + /// + NORMAL_CONVERSATION_REQUIRED = 4314, + + + /// + /// 当前用户被加入此对话的黑名单,无法发送消息。 + /// + CONVERSATION_BLACKLISTED = 4315, + + + /// + /// 该功能仅对暂态对话有效。 + /// + TRANSIENT_CONVERSATION_REQUIRED = 4316, + + /// + /// 发送消息的对话不存在,或当前用户不在对话中 + /// + INVALID_MESSAGING_TARGET = 4401, + + /// + /// 发送的消息被应用的云引擎 Hook 拒绝 + /// + MESSAGE_REJECTED_BY_APP = 4402, + + /// + /// 客户端无法通过 WebSocket 发送数据包 + /// + CAN_NOT_EXCUTE_COMMAND = 1002, + + } + /// + /// 用户云代码返回的错误码 + /// + public int AppCode { get; private set; } + + + internal AVIMException(ErrorCode code, string message, Exception cause = null) + : base(message, cause) + { + this.Code = code; + } + + internal AVIMException(int code, int appCode, string message, Exception cause = null) + : this((ErrorCode)code, message, cause) + { + this.AppCode = appCode; + } + + /// + /// The LeanCloud error code associated with the exception. + /// + public ErrorCode Code { get; private set; } + } +} diff --git a/RTM/Source/Public/AVIMImageMessage.cs b/RTM/Source/Public/AVIMImageMessage.cs new file mode 100644 index 0000000..9e00b67 --- /dev/null +++ b/RTM/Source/Public/AVIMImageMessage.cs @@ -0,0 +1,246 @@ +using LeanCloud; +using LeanCloud.Storage.Internal; +using LeanCloud.Realtime.Internal; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.IO; + +namespace LeanCloud.Realtime +{ + /// + /// 图像消息 + /// + [AVIMMessageClassName("_AVIMImageMessage")] + [AVIMTypedMessageTypeInt(-2)] + public class AVIMImageMessage : AVIMFileMessage + { + + } + + /// + /// File message. + /// + [AVIMMessageClassName("_AVIMFileMessage")] + [AVIMTypedMessageTypeInt(-6)] + public class AVIMFileMessage : AVIMMessageDecorator + { + /// + /// Initializes a new instance of the class. + /// + public AVIMFileMessage() + : base(new AVIMTypedMessage()) + { + + } + + /// + /// Initializes a new instance of the class. + /// + /// Message. + public AVIMFileMessage(AVIMTypedMessage message) + : base(message) + { + + } + + /// + /// Gets or sets the file. + /// + /// The file. + public AVFile File { get; set; } + + /// + /// Froms the URL. + /// + /// The URL. + /// URL. + /// The 1st type parameter. + public static T FromUrl(string url) where T : AVIMFileMessage, new() + { + return FromUrl(string.Empty.Random(8), url, null); + } + + /// + /// From the URL. + /// + /// The URL. + /// File name. + /// External URL. + /// Text title. + /// Custom attributes. + /// The 1st type parameter. + public static T FromUrl(string fileName, string externalUrl, string textTitle, IDictionary customAttributes = null) where T : AVIMFileMessage, new() + { + T message = new T(); + message.File = new AVFile(fileName, externalUrl); + message.TextContent = textTitle; + message.MergeCustomAttributes(customAttributes); + return message; + } + + /// + /// From the stream. + /// + /// The stream. + /// File name. + /// Data. + /// MIME type. + /// Text title. + /// Meta data. + /// Custom attributes. + /// The 1st type parameter. + public static T FromStream(string fileName, Stream data, string mimeType, string textTitle, IDictionary metaData, IDictionary customAttributes = null) where T : AVIMFileMessage, new() + { + T message = new T(); + message.File = new AVFile(fileName, data, mimeType, metaData); + message.TextContent = textTitle; + message.MergeCustomAttributes(customAttributes); + return message; + } + + /// + /// Encodes the decorator. + /// + /// The decorator. + public override IDictionary EncodeDecorator() + { + if (File.Url == null) throw new InvalidOperationException("File.Url can not be null before it can be sent."); + File.MetaData["name"] = File.Name; + File.MetaData["format"] = File.MimeType; + var fileData = new Dictionary + { + { "url", File.Url.ToString()}, + { "objId", File.ObjectId }, + { "metaData", File.MetaData } + }; + + return new Dictionary + { + { AVIMProtocol.LCFILE, fileData } + }; + } + + /// + /// Deserialize the specified msgStr. + /// + /// The deserialize. + /// Message string. + public override IAVIMMessage Deserialize(string msgStr) + { + var msg = Json.Parse(msgStr) as IDictionary; + var fileData = msg[AVIMProtocol.LCFILE] as IDictionary; + string mimeType = null; + string url = null; + string name = null; + string objId = null; + IDictionary metaData = null; + if (fileData != null) + { + object metaDataObj = null; + + if (fileData.TryGetValue("metaData", out metaDataObj)) + { + metaData = metaDataObj as IDictionary; + object fileNameObj = null; + if (metaData != null) + { + if (metaData.TryGetValue("name", out fileNameObj)) + { + name = fileNameObj.ToString(); + } + } + object mimeTypeObj = null; + if (metaData != null) + { + if (metaData.TryGetValue("format", out mimeTypeObj)) + { + if (mimeTypeObj != null) + mimeType = mimeTypeObj.ToString(); + } + } + } + + object objIdObj = null; + if (fileData.TryGetValue("objId", out objIdObj)) + { + if (objIdObj != null) + objId = objIdObj.ToString(); + } + + object urlObj = null; + if (fileData.TryGetValue("url", out urlObj)) + { + url = urlObj.ToString(); + } + File = AVFile.CreateWithData(objId, name, url, metaData); + } + + return base.Deserialize(msgStr); + } + } + + /// + /// Location message. + /// + [AVIMMessageClassName("_AVIMMessageClassName")] + [AVIMTypedMessageTypeInt(-5)] + public class AVIMLocationMessage : AVIMMessageDecorator + { + /// + /// Initializes a new instance of the class. + /// + public AVIMLocationMessage() + : base(new AVIMTypedMessage()) + { + + } + + /// + /// Gets or sets the location. + /// + /// The location. + public AVGeoPoint Location { get; set; } + + public AVIMLocationMessage(AVGeoPoint location) + : this() + { + Location = location; + } + + /// + /// Encodes the decorator. + /// + /// The decorator. + public override IDictionary EncodeDecorator() + { + var locationData = new Dictionary + { + { "longitude", Location.Longitude}, + { "latitude", Location.Latitude } + }; + return new Dictionary + { + { AVIMProtocol.LCLOC, locationData } + }; + } + + /// + /// Deserialize the specified msgStr. + /// + /// The deserialize. + /// Message string. + public override IAVIMMessage Deserialize(string msgStr) + { + var msg = Json.Parse(msgStr) as IDictionary; + var locationData = msg[AVIMProtocol.LCLOC] as IDictionary; + if (locationData != null) + { + Location = new AVGeoPoint(double.Parse(locationData["latitude"].ToString()), double.Parse(locationData["longitude"].ToString())); + } + return base.Deserialize(msgStr); + } + } +} diff --git a/RTM/Source/Public/AVIMMessage.cs b/RTM/Source/Public/AVIMMessage.cs new file mode 100644 index 0000000..21fb38d --- /dev/null +++ b/RTM/Source/Public/AVIMMessage.cs @@ -0,0 +1,162 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using LeanCloud; +using System.Reflection; +using LeanCloud.Storage.Internal; +using System.Threading; +using System.Collections; +using LeanCloud.Realtime.Internal; + +namespace LeanCloud.Realtime +{ + /// + /// 实时消息的核心基类,它是 Json schema 消息的父类 + /// + [AVIMMessageClassName("_AVIMMessage")] + public class AVIMMessage : IAVIMMessage + { + /// + /// 默认的构造函数 + /// + public AVIMMessage() + { + + } + internal readonly object mutex = new object(); + + /// + /// 对话的Id + /// + public string ConversationId { get; set; } + + /// + /// 发送消息的 ClientId + /// + public string FromClientId { get; set; } + + /// + /// 消息在全局的唯一标识Id + /// + public string Id { get; set; } + + /// + /// 服务器端的时间戳 + /// + public long ServerTimestamp { get; set; } + + /// + /// Gets or sets the content. + /// + /// The content. + public string Content { get; set; } + + /// + /// 对方收到消息的时间戳,如果是多人聊天,那以最早收到消息的人回发的 ACK 为准 + /// + public long RcpTimestamp { get; set; } + + public long UpdatedAt { get; set; } + + internal string cmdId { get; set; } + + #region + /// + /// Gets or sets a value indicating whether this mention all. + /// + /// true if mention all; otherwise, false. + public bool MentionAll { get; set; } + + /// + /// Gets or sets the mention list. + /// + /// The mention list. + public IEnumerable MentionList { get; set; } + + #endregion + + #region register convertor for custom typed message + + /// + /// Serialize this message. + /// + /// The serialize. + public virtual string Serialize() + { + return Content; + } + + /// + /// Validate the specified msgStr. + /// + /// The validate. + /// Message string. + public virtual bool Validate(string msgStr) + { + return true; + } + + /// + /// Deserialize the specified msgStr to message subclass instance + /// + /// The deserialize. + /// Message string. + public virtual IAVIMMessage Deserialize(string msgStr) + { + Content = msgStr; + return this; + } + + internal virtual MessageCommand BeforeSend(MessageCommand cmd) + { + return cmd; + } + + internal static IAVIMMessage CopyMetaData(IAVIMMessage srcMsg, IAVIMMessage desMsg) { + if (srcMsg == null) + return desMsg; + + desMsg.ConversationId = srcMsg.ConversationId; + desMsg.FromClientId = srcMsg.FromClientId; + desMsg.Id = srcMsg.Id; + desMsg.ServerTimestamp = srcMsg.ServerTimestamp; + desMsg.RcpTimestamp = srcMsg.RcpTimestamp; + desMsg.UpdatedAt = srcMsg.UpdatedAt; + return desMsg; + } + + #endregion + } + + + /// + /// 消息的发送选项 + /// + public struct AVIMSendOptions + { + /// + /// 是否需要送达回执 + /// + public bool Receipt; + /// + /// 是否是暂态消息,暂态消息不返回送达回执(ack),不保留离线消息,不触发离线推送 + /// + public bool Transient; + /// + /// 消息的优先级,默认是1,可选值还有 2|3 + /// + public int Priority; + /// + /// 是否为 Will 类型的消息,这条消息会被缓存在服务端,一旦当前客户端下线,这条消息会被发送到对话内的其他成员 + /// + public bool Will; + + /// + /// 如果消息的接收者已经下线了,这个字段的内容就会被离线推送到接收者 + ///例如,一张图片消息的离线消息内容可以类似于:[您收到一条图片消息,点击查看] 这样的推送内容,参照微信的做法 + /// + public IDictionary PushData; + } +} diff --git a/RTM/Source/Public/AVIMMessageClassNameAttribute.cs b/RTM/Source/Public/AVIMMessageClassNameAttribute.cs new file mode 100644 index 0000000..e4d7228 --- /dev/null +++ b/RTM/Source/Public/AVIMMessageClassNameAttribute.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LeanCloud.Realtime +{ + [AttributeUsage(AttributeTargets.Class, Inherited = true, AllowMultiple = false)] + public sealed class AVIMMessageClassNameAttribute: Attribute + { + public AVIMMessageClassNameAttribute(string className) + { + this.ClassName = className; + } + public string ClassName { get; private set; } + + } +} diff --git a/RTM/Source/Public/AVIMMessageFieldNameAttribute.cs b/RTM/Source/Public/AVIMMessageFieldNameAttribute.cs new file mode 100644 index 0000000..0e155b5 --- /dev/null +++ b/RTM/Source/Public/AVIMMessageFieldNameAttribute.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace LeanCloud.Realtime +{ + [AttributeUsage(AttributeTargets.Property, Inherited = true, AllowMultiple = false)] + public sealed class AVIMMessageFieldNameAttribute: Attribute + { + public AVIMMessageFieldNameAttribute(string fieldName) + { + FieldName = fieldName; + } + + public string FieldName { get; private set; } + } +} diff --git a/RTM/Source/Public/AVIMMessageListener.cs b/RTM/Source/Public/AVIMMessageListener.cs new file mode 100644 index 0000000..d0d91cf --- /dev/null +++ b/RTM/Source/Public/AVIMMessageListener.cs @@ -0,0 +1,143 @@ +using LeanCloud.Realtime.Internal; +using LeanCloud.Storage.Internal; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LeanCloud.Realtime +{ + /// + /// 默认的消息监听器,它主要承担的指责是回执的发送与用户自定义的监听器不冲突 + /// + public class AVIMMessageListener : IAVIMListener + { + /// + /// 默认的 AVIMMessageListener 只会监听 direct 协议,但是并不会触发针对消息类型的判断的监听器 + /// + public AVIMMessageListener() + { + + } + + /// + /// Protocols the hook. + /// + /// true, if hook was protocoled, false otherwise. + /// Notice. + public virtual bool ProtocolHook(AVIMNotice notice) + { + if (notice.CommandName != "direct") return false; + if (notice.RawData.ContainsKey("offline")) return false; + return true; + } + + private EventHandler m_OnMessageReceived; + /// + /// 接收到聊天消息的事件通知 + /// + public event EventHandler OnMessageReceived + { + add + { + m_OnMessageReceived += value; + } + remove + { + m_OnMessageReceived -= value; + } + } + internal virtual void OnMessage(AVIMNotice notice) + { + if (m_OnMessageReceived != null) + { + var msgStr = notice.RawData["msg"].ToString(); + var iMessage = AVRealtime.FreeStyleMessageClassingController.Instantiate(msgStr, notice.RawData); + //var messageNotice = new AVIMMessageNotice(notice.RawData); + //var messaegObj = AVIMMessage.Create(messageNotice); + var args = new AVIMMessageEventArgs(iMessage); + m_OnMessageReceived.Invoke(this, args); + } + } + + /// + /// Ons the notice received. + /// + /// Notice. + public virtual void OnNoticeReceived(AVIMNotice notice) + { + this.OnMessage(notice); + } + + } + + /// + /// 文本消息监听器 + /// + public class AVIMTextMessageListener : IAVIMListener + { + /// + /// 构建默认的文本消息监听器 + /// + public AVIMTextMessageListener() + { + + } + + /// + /// 构建文本消息监听者 + /// + /// + public AVIMTextMessageListener(Action textMessageReceived) + { + OnTextMessageReceived += (sender, textMessage) => + { + textMessageReceived(textMessage.TextMessage); + }; + } + + private EventHandler m_OnTextMessageReceived; + public event EventHandler OnTextMessageReceived + { + add + { + m_OnTextMessageReceived += value; + } + remove + { + m_OnTextMessageReceived -= value; + } + } + + public virtual bool ProtocolHook(AVIMNotice notice) + { + if (notice.CommandName != "direct") return false; + try + { + var msg = Json.Parse(notice.RawData["msg"].ToString()) as IDictionary; + if (!msg.Keys.Contains(AVIMProtocol.LCTYPE)) return false; + var typInt = 0; + int.TryParse(msg[AVIMProtocol.LCTYPE].ToString(), out typInt); + if (typInt != -1) return false; + return true; + } + catch(ArgumentException) + { + + } + return false; + + } + + public virtual void OnNoticeReceived(AVIMNotice notice) + { + if (m_OnTextMessageReceived != null) + { + var textMessage = new AVIMTextMessage(); + textMessage.Deserialize(notice.RawData["msg"].ToString()); + m_OnTextMessageReceived(this, new AVIMTextMessageEventArgs(textMessage)); + } + } + } +} diff --git a/RTM/Source/Public/AVIMNotice.cs b/RTM/Source/Public/AVIMNotice.cs new file mode 100644 index 0000000..65f4053 --- /dev/null +++ b/RTM/Source/Public/AVIMNotice.cs @@ -0,0 +1,57 @@ +using LeanCloud; +using LeanCloud.Realtime.Internal; +using LeanCloud.Storage.Internal; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace LeanCloud.Realtime +{ + /// + /// + /// + public interface IAVIMNotice + { + /// + /// + /// + /// + /// + AVIMNotice Restore(IDictionary estimatedData); + } + /// + /// 从服务端接受到的通知 + /// 通知泛指消息,对话信息变更(例如加人和被踢等),服务器的 ACK,消息回执等 + /// + public class AVIMNotice : EventArgs + { + /// + /// Initializes a new instance of the class. + /// + public AVIMNotice() + { + + } + + /// + /// The name of the command. + /// + public readonly string CommandName; + public readonly IDictionary RawData; + public AVIMNotice(IDictionary estimatedData) + { + this.RawData = estimatedData; + this.CommandName = estimatedData["cmd"].ToString(); + } + + public static bool IsValidLeanCloudProtocol(IDictionary estimatedData) + { + if (estimatedData == null) return false; + if (estimatedData.Count == 0) return false; + return true; + } + } +} diff --git a/RTM/Source/Public/AVIMRecalledMessage.cs b/RTM/Source/Public/AVIMRecalledMessage.cs new file mode 100644 index 0000000..1e863fd --- /dev/null +++ b/RTM/Source/Public/AVIMRecalledMessage.cs @@ -0,0 +1,12 @@ +using System; + +namespace LeanCloud.Realtime { + /// + /// 撤回消息 + /// + [AVIMMessageClassName("_AVIMRecalledMessagee")] + [AVIMTypedMessageTypeInt(-127)] + public class AVIMRecalledMessage : AVIMTypedMessage { + + } +} diff --git a/RTM/Source/Public/AVIMSignature.cs b/RTM/Source/Public/AVIMSignature.cs new file mode 100644 index 0000000..c5bd0d3 --- /dev/null +++ b/RTM/Source/Public/AVIMSignature.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LeanCloud.Realtime +{ + /// + /// 签名 + /// + public class AVIMSignature + { + /// + /// 经过 SHA1 以及相关操作参数计算出来的加密字符串 + /// + public string SignatureContent { get; set; } + + /// + /// 服务端时间戳 + /// + public long Timestamp { get; set; } + + /// + /// 随机字符串 + /// + public string Nonce { get; set; } + + /// + /// 构造一个签名 + /// + /// + /// + /// + public AVIMSignature(string s,long t,string n) + { + this.Nonce = n; + this.SignatureContent = s; + this.Timestamp = t; + } + } +} diff --git a/RTM/Source/Public/AVIMTemporaryConversation.cs b/RTM/Source/Public/AVIMTemporaryConversation.cs new file mode 100644 index 0000000..1e06769 --- /dev/null +++ b/RTM/Source/Public/AVIMTemporaryConversation.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LeanCloud.Realtime +{ + /// + /// Temporary conversation. + /// + public class AVIMTemporaryConversation : AVIMConversation + { + public DateTime ExpiredAt + { + get + { + if (expiredAt == null) + return DateTime.Now.AddDays(1); + return expiredAt.Value; + } + + set + { + expiredAt = value; + } + } + + internal AVIMTemporaryConversation(long ttl) + : base(isTemporary: true) + { + this.expiredAt = DateTime.Now.AddDays(1); + } + } + + +} diff --git a/RTM/Source/Public/AVIMTextMessage.cs b/RTM/Source/Public/AVIMTextMessage.cs new file mode 100644 index 0000000..7bc1d34 --- /dev/null +++ b/RTM/Source/Public/AVIMTextMessage.cs @@ -0,0 +1,47 @@ +using LeanCloud.Realtime.Internal; +using LeanCloud.Storage.Internal; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LeanCloud.Realtime +{ + /// + /// 纯文本信息 + /// + [AVIMMessageClassName("_AVIMTextMessage")] + [AVIMTypedMessageTypeInt(-1)] + public class AVIMTextMessage : AVIMTypedMessage + { + /// + /// 构建一个文本信息 class. + /// + public AVIMTextMessage() + { + + } + + /// + /// 文本类型标记 + /// + [Obsolete("LCType is deprecated, please use AVIMTypedMessageTypeInt instead.")] + [AVIMMessageFieldName("_lctype")] + public int LCType + { + get; set; + } + + /// + /// 构造一个纯文本信息 + /// + /// + public AVIMTextMessage(string textContent) + : this() + { + TextContent = textContent; + } + } +} diff --git a/RTM/Source/Public/AVIMTypedMessage.cs b/RTM/Source/Public/AVIMTypedMessage.cs new file mode 100644 index 0000000..c756e1f --- /dev/null +++ b/RTM/Source/Public/AVIMTypedMessage.cs @@ -0,0 +1,205 @@ +using LeanCloud.Storage.Internal; +using LeanCloud.Realtime.Internal; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace LeanCloud.Realtime +{ + /// + /// + /// + [AVIMMessageClassName("_AVIMTypedMessage")] + [AVIMTypedMessageTypeInt(0)] + public class AVIMTypedMessage : AVIMMessage, IEnumerable> + { + /// + /// Initializes a new instance of the class. + /// + public AVIMTypedMessage() + { + + } + + /// + /// 文本内容 + /// + [AVIMMessageFieldName("_lctext")] + public string TextContent + { + get; set; + } + + private IDictionary estimatedData = new Dictionary(); + /// + /// Serialize this instance. + /// + /// The serialize. + public override string Serialize() + { + var result = Encode(); + var resultStr = Json.Encode(result); + this.Content = resultStr; + return resultStr; + } + + /// + /// Encode this instance. + /// + /// The encode. + public virtual IDictionary Encode() + { + var result = AVRealtime.FreeStyleMessageClassingController.EncodeProperties(this); + var encodedAttrs = PointerOrLocalIdEncoder.Instance.Encode(estimatedData); + result[AVIMProtocol.LCATTRS] = estimatedData; + return result; + } + + /// + /// Validate the specified msgStr. + /// + /// The validate. + /// Message string. + public override bool Validate(string msgStr) + { + try + { + var msg = Json.Parse(msgStr) as IDictionary; + return msg.ContainsKey(AVIMProtocol.LCTYPE); + } + catch + { + + } + return false; + } + + /// + /// Deserialize the specified msgStr. + /// + /// The deserialize. + /// Message string. + public override IAVIMMessage Deserialize(string msgStr) + { + var msg = Json.Parse(msgStr) as IDictionary; + var className = AVRealtime.FreeStyleMessageClassingController.GetClassName(this.GetType()); + var PropertyMappings = AVRealtime.FreeStyleMessageClassingController.GetPropertyMappings(className); + var messageFieldProperties = PropertyMappings.Where(prop => msg.ContainsKey(prop.Value)) + .Select(prop => Tuple.Create(ReflectionHelpers.GetProperty(this.GetType(), prop.Key), msg[prop.Value])); + + foreach (var property in messageFieldProperties) + { + property.Item1.SetValue(this, property.Item2, null); + } + + if (msg.ContainsKey(AVIMProtocol.LCATTRS)) + { + object attrs = msg[AVIMProtocol.LCATTRS]; + this.estimatedData = AVDecoder.Instance.Decode(attrs) as Dictionary; + } + + return base.Deserialize(msgStr); + } + + /// + /// Gets or sets the with the specified key. + /// + /// Key. + public virtual object this[string key] + { + get + { + if (estimatedData.TryGetValue(key, out object value)) { + return value; + } + return null; + } + set + { + estimatedData[key] = value; + } + } + + /// + /// Merges the custom attributes. + /// + /// Custom attributes. + public void MergeCustomAttributes(IDictionary customAttributes) + { + this.estimatedData = this.estimatedData.Merge(customAttributes); + } + + /// + /// Gets the enumerator. + /// + /// The enumerator. + public IEnumerator> GetEnumerator() + { + return estimatedData.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable>)this).GetEnumerator(); + } + } + + /// + /// AVIMMessage decorator. + /// + public abstract class AVIMMessageDecorator : AVIMTypedMessage + { + /// + /// Gets or sets the message. + /// + /// The message. + public AVIMTypedMessage Message { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// Message. + protected AVIMMessageDecorator(AVIMTypedMessage message) + { + this.Message = message; + } + + /// + /// Gets or sets the content of the message. + /// + /// The content of the message. + public virtual IDictionary MessageContent { get; set; } + + /// + /// Encodes the decorated. + /// + /// The decorated. + public virtual IDictionary EncodeDecorated() + { + return Message.Encode(); + } + + /// + /// Encode this instance. + /// + /// The encode. + public override IDictionary Encode() + { + var decoratedMessageEncoded = EncodeDecorated(); + var selfEncoded = base.Encode(); + var decoratoEncoded = this.EncodeDecorator(); + var resultEncoed = decoratedMessageEncoded.Merge(selfEncoded).Merge(decoratoEncoded); + return resultEncoed; + } + + /// + /// Encodes the decorator. + /// + /// The decorator. + public abstract IDictionary EncodeDecorator(); + } + + +} diff --git a/RTM/Source/Public/AVIMTypedMessageTypeIntAttribute.cs b/RTM/Source/Public/AVIMTypedMessageTypeIntAttribute.cs new file mode 100644 index 0000000..49c4b76 --- /dev/null +++ b/RTM/Source/Public/AVIMTypedMessageTypeIntAttribute.cs @@ -0,0 +1,14 @@ +using System; +namespace LeanCloud.Realtime +{ + [AttributeUsage(AttributeTargets.Class, Inherited = true, AllowMultiple = false)] + public sealed class AVIMTypedMessageTypeIntAttribute : Attribute + { + public AVIMTypedMessageTypeIntAttribute(int typeInt) + { + this.TypeInteger = typeInt; + } + + public int TypeInteger { get; private set; } + } +} diff --git a/RTM/Source/Public/AVRealtime.cs b/RTM/Source/Public/AVRealtime.cs new file mode 100644 index 0000000..cd6360d --- /dev/null +++ b/RTM/Source/Public/AVRealtime.cs @@ -0,0 +1,1294 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Text; +using System.Threading.Tasks; +using LeanCloud; +using System.Reflection; +using LeanCloud.Realtime.Internal; +using LeanCloud.Storage.Internal; +using System.Threading; + +#if UNITY +using UnityEngine; +#endif +namespace LeanCloud.Realtime +{ + + /// + /// 实时消息的框架类 + /// 包含了 WebSocket 连接以及事件通知的管理 + /// + public class AVRealtime + { + internal static IDictionary clients = null; + + private static readonly object mutex = new object(); + private string _wss; + private string _secondaryWss; + private string _sesstionToken; + private long _sesstionTokenExpire; + private string _clientId; + private string _deviceId; + private bool _secure; + private string _tag; + private string subprotocolPrefix = "lc.json."; + + static readonly int RECONNECT_DELAY = 5 * 1000; + static readonly int RECONNECT_USE_SECONDARY_TIMES = 6; + static readonly int RECONNECT_FROM_APP_ROUTER = 12; + + int reconnectTimes; + + public bool IsSesstionTokenExpired + { + get + { + return DateTime.Now.ToUnixTimeStamp() > _sesstionTokenExpire; + } + } + + + + private IAVIMCommandRunner avIMCommandRunner; + + + /// + /// + /// + public IAVIMCommandRunner AVIMCommandRunner + { + get + { + lock (mutex) + { + avIMCommandRunner = avIMCommandRunner ?? new AVIMCommandRunner(this.AVWebSocketClient); + return avIMCommandRunner; + } + } + } + + private IWebSocketClient webSocketController; + internal IWebSocketClient AVWebSocketClient + { + get + { + lock (mutex) + { + webSocketController = webSocketController ?? new DefaultWebSocketClient(); + return webSocketController; + } + } + set + { + lock (mutex) + { + webSocketController = value; + } + } + } + + internal static IAVRouterController RouterController + { + get + { + return AVIMCorePlugins.Instance.RouterController; + } + } + + internal static IFreeStyleMessageClassingController FreeStyleMessageClassingController + { + get + { + return AVIMCorePlugins.Instance.FreeStyleClassingController; + } + } + + /// + /// + /// + public event EventHandler OnOfflineMessageReceived; + + /// + /// 与云端通讯的状态 + /// + public enum Status + { + /// + /// 未初始化 + /// + None = -1, + + /// + /// 正在连接 + /// + Connecting = 0, + + /// + /// 已连接 + /// + Online = 1, + + /// + /// 连接已断开 + /// + Offline = 2, + + /// + /// 正在重连 + /// + Reconnecting = 3, + + /// + /// websocket 连接已被打开 + /// + Opened = 98, + + /// + /// 已主动关闭 + /// + Closed = 99, + } + + private AVRealtime.Status state = Status.None; + public AVRealtime.Status State + { + get + { + return state; + } + private set + { + state = value; + } + } + + private struct NetworkStateOptions + { + public bool Available { get; set; } + } + + private NetworkStateOptions NetworkState { get; set; } + + private struct WebSocketStateOptions + { + public int ClosedCode { get; set; } + } + + private WebSocketStateOptions WebSocketState { get; set; } + + /// + /// + /// + public struct AVIMReconnectOptions + { + /// + /// 重连的时间间隔,单位是秒 + /// + public long Interval { get; set; } + + /// + /// 重连的次数 + /// + public int Retry { get; set; } + } + + internal string Subprotocol + { + get + { + return subprotocolPrefix + (int)CurrentConfiguration.OfflineMessageStrategy; + } + } + + /// + /// 重连选项 + /// + public AVIMReconnectOptions ReconnectOptions { get; set; } + + private ISignatureFactory _signatureFactory; + + /// + /// 签名接口 + /// + public ISignatureFactory SignatureFactory + { + get + { + _signatureFactory = _signatureFactory ?? new DefaulSiganatureFactory(); + return _signatureFactory; + } + set + { + _signatureFactory = value; + } + } + + private bool useLeanEngineSignaturFactory; + /// + /// 启用 LeanEngine 云函数签名 + /// + public void UseLeanEngineSignatureFactory() + { + useLeanEngineSignaturFactory = true; + this.SignatureFactory = new LeanEngineSignatureFactory(); + } + + private EventHandler m_OnDisconnected; + /// + /// 连接断开触发的事件 + /// 如果其他客户端使用了相同的 Tag 登录,就会导致当前用户被服务端断开 + /// + public event EventHandler OnDisconnected + { + add + { + m_OnDisconnected += value; + } + remove + { + m_OnDisconnected -= value; + } + } + + private EventHandler m_OnReconnecting; + /// + /// 正在重连时触发的事件 + /// + public event EventHandler OnReconnecting + { + add + { + m_OnReconnecting += value; + } + remove + { + m_OnReconnecting -= value; + } + } + + private EventHandler m_OnReconnected; + /// + /// 重连之后触发的事件 + /// + public event EventHandler OnReconnected + { + add + { + m_OnReconnected += value; + } + remove + { + m_OnReconnected -= value; + } + } + + private EventHandler m_OnReconnectFailed; + + /// + /// 重连失败之后触发的事件 + /// + public event EventHandler OnReconnectFailed + { + add + { + m_OnReconnectFailed += value; + } + remove + { + m_OnReconnectFailed -= value; + } + } + + /// + /// Invokes the state of the network. + /// + /// If set to true broken. + internal void InvokeNetworkState(bool available = false) + { + if (this.NetworkState.Available == available) return; + SetNetworkState(available); + PrintLog(string.Format("network connectivity is {0} now", available)); + // 如果断线产生的原因是客户端掉线而不是服务端踢下线,则应该开始自动重连 + var reasonShouldReconnect = new int[] { 0, 1006, 4107 }; + if (this.NetworkState.Available && reasonShouldReconnect.Contains(this.WebSocketState.ClosedCode)) + { + StartAutoReconnect(); + } + } + + internal void SetNetworkState(bool available = true) + { + this.NetworkState = new NetworkStateOptions() + { + Available = available + }; + } + + private EventHandler m_NoticeReceived; + public event EventHandler NoticeReceived + { + add + { + m_NoticeReceived += value; + } + remove + { + m_NoticeReceived -= value; + } + } + + private void WebSocketClient_OnMessage(string obj) + { + try + { + var estimatedData = Json.Parse(obj) as IDictionary; + var validator = AVIMNotice.IsValidLeanCloudProtocol(estimatedData); + if (validator) + { + var notice = new AVIMNotice(estimatedData); + m_NoticeReceived?.Invoke(this, notice); + } + } + catch (Exception ex) + { + if (ex.InnerException != null) + { + PrintLog(ex.InnerException.Source); + } + if (ex.Source != null) + { + PrintLog(ex.Source); + } + + PrintLog(ex.Message); + } + } + + /// + /// 设定监听者 + /// + /// + /// + public void SubscribeNoticeReceived(IAVIMListener listener, Func runtimeHook = null) + { + this.NoticeReceived += new EventHandler((sender, notice) => + { + var approved = runtimeHook == null ? listener.ProtocolHook(notice) : runtimeHook(notice) && listener.ProtocolHook(notice); + if (approved) + { + listener.OnNoticeReceived(notice); + } + }); + } + + /// + /// 初始化配置项 + /// + public struct Configuration + { + /// + /// 签名工厂 + /// + public ISignatureFactory SignatureFactory { get; set; } + /// + /// 自定义 WebSocket 客户端 + /// + public IWebSocketClient WebSocketClient { get; set; } + /// + /// LeanCloud App Id + /// + public string ApplicationId { get; set; } + /// + /// LeanCloud App Key + /// + public string ApplicationKey { get; set; } + + /// + /// 登录的时候告知服务器,本次登录所使用的离线消息策略 + /// + public OfflineMessageStrategy OfflineMessageStrategy { get; set; } + + /// + /// Gets or sets the realtime server. + /// + /// The realtime server. + public Uri RealtimeServer { get; set; } + + /// + /// Gets or sets the push router server. + /// + /// The push router server. + public Uri RTMRouter { get; set; } + } + + /// + ///登录时的离线消息下发策略 + /// + public enum OfflineMessageStrategy + { + /// + /// 服务器将所有离线消息一次性在登录之后马上下发下来 + /// + Default = 1, + + /// + /// 不再下发未读消息,而是下发对话的未读通知,告知客户端有哪些对话处于未读状态 + /// + [Obsolete("该策略已被废弃,请使用 UnreadAck")] + UnreadNotice = 2, + + /// + /// ack 和 read 分离, ack 不会清理未读消息 + /// + UnreadAck = 3 + } + + /// + /// 当前配置 + /// + public Configuration CurrentConfiguration { get; internal set; } + /// + /// 初始化实时消息客户端 + /// + /// + public AVRealtime(Configuration config) + { + lock (mutex) + { + if ((int)config.OfflineMessageStrategy == 0) + { + config.OfflineMessageStrategy = OfflineMessageStrategy.UnreadAck; + } + + CurrentConfiguration = config; + if (CurrentConfiguration.WebSocketClient != null) + { + webSocketController = CurrentConfiguration.WebSocketClient; + } + if (CurrentConfiguration.SignatureFactory != null) + { + this.SignatureFactory = CurrentConfiguration.SignatureFactory; + } + ReconnectOptions = new AVIMReconnectOptions() + { + Interval = 5, + Retry = 120 + }; + + + RegisterMessageType(); + RegisterMessageType(); + RegisterMessageType(); + RegisterMessageType(); + RegisterMessageType(); + RegisterMessageType(); + RegisterMessageType(); + RegisterMessageType(); + RegisterMessageType(); + + // 注册服务端 goaway 指令 + var goAwayListener = new GoAwayListener(); + goAwayListener.OnGoAway += () => { + RouterController.ClearCache().ContinueWith(_ => { + reborn = true; + // 关闭 WebSocket + AVWebSocketClient.Disconnect(); + }); + }; + SubscribeNoticeReceived(goAwayListener); + + reconnectTimes = 0; + } + } + + /// + /// 初始化实时消息客户端 + /// + /// + /// + public AVRealtime(string applicationId, string applicationKey) + : this(new Configuration() + { + ApplicationId = applicationId, + ApplicationKey = applicationKey, + OfflineMessageStrategy = OfflineMessageStrategy.UnreadAck + }) + { + + } + + #region websocket log + internal static Action LogTracker { get; private set; } + /// + /// 打开 WebSocket 日志 + /// + /// + public static void WebSocketLog(Action trace) + { + LogTracker = trace; + } + public static void PrintLog(string log) + { + if (AVRealtime.LogTracker != null) + { + AVRealtime.LogTracker(log); + } + } + #endregion + + /// + /// 创建 Client + /// + /// + /// + /// 设备唯一的 Id。如果是 iOS 设备,需要将 iOS 推送使用的 DeviceToken 作为 deviceId 传入 + /// 是否强制加密 wss 链接 + /// + /// + public Task CreateClientAsync(string clientId, + string tag = null, + string deviceId = null, + bool secure = true, + CancellationToken cancellationToken = default(CancellationToken)) + { + lock (mutex) + { + var client = PreLogIn(clientId, tag, deviceId); + + AVRealtime.PrintLog("begin OpenAsync."); + return OpenAsync(secure, Subprotocol, true, cancellationToken).OnSuccess(t => + { + if (!t.Result) + { + return Task.FromResult(null); + } + AVRealtime.PrintLog("websocket server connected, begin to open sesstion."); + SetNetworkState(); + var cmd = new SessionCommand() + .UA(VersionString) + .Tag(tag) + .DeviceId(deviceId) + .Option("open") + .PeerId(clientId); + + ToggleNotification(true); + return AttachSignature(cmd, this.SignatureFactory.CreateConnectSignature(clientId)); + + }).Unwrap().OnSuccess(x => + { + var cmd = x.Result; + if (cmd == null) + { + return Task.FromResult>>(null); + } + return this.RunCommandAsync(cmd); + }).Unwrap().OnSuccess(s => + { + if (s.Result == null) + { + return null; + } + AVRealtime.PrintLog("sesstion opened."); + state = Status.Online; + ToggleHeartBeating(true); + var response = s.Result.Item2; + if (response.ContainsKey("st")) + { + _sesstionToken = response["st"] as string; + } + if (response.ContainsKey("stTtl")) + { + var stTtl = long.Parse(response["stTtl"].ToString()); + _sesstionTokenExpire = DateTime.Now.ToUnixTimeStamp() + stTtl * 1000; + } + AfterLogIn(client); + return client; + }); + } + } + + + + /// + /// Creates the client async. + /// + /// The client async. + /// User. + /// Tag. + /// Device identifier. + /// If set to true secure. + public Task CreateClientAsync(AVUser user = null, + string tag = null, + string deviceId = null, + bool secure = true, + CancellationToken cancellationToken = default(CancellationToken)) + { + AVIMClient client = null; + AVRealtime.PrintLog("begin OpenAsync."); + return OpenAsync(secure, Subprotocol, true, cancellationToken).OnSuccess(openTask => + { + AVRealtime.PrintLog("OpenAsync OnSuccess. begin send open sesstion cmd."); + var userTask = Task.FromResult(user); + if (user == null) + userTask = AVUser.GetCurrentUserAsync(); + + return userTask; + }).Unwrap().OnSuccess(u => + { + var theUser = u.Result; + return AVCloud.RequestRealtimeSignatureAsync(theUser); + }).Unwrap().OnSuccess(signTask => + { + var signResult = signTask.Result; + var clientId = signResult.ClientId; + var nonce = signResult.Nonce; + var singnature = signResult.Signature; + var ts = signResult.Timestamp; + + client = PreLogIn(clientId, tag, deviceId); + ToggleNotification(true); + return this.OpenSessionAsync(clientId, tag, deviceId, nonce, ts, singnature, secure); + }).Unwrap().OnSuccess(s => + { + ToggleHeartBeating(true); + AfterLogIn(client); + return client; + }); + } + + #region pre-login + internal AVIMClient PreLogIn(string clientId, + string tag = null, + string deviceId = null) + { + var client = new AVIMClient(clientId, tag, this); + if (this.OnOfflineMessageReceived != null) + { + client.OnOfflineMessageReceived += this.OnOfflineMessageReceived; + } + _clientId = clientId; + _tag = tag; + _deviceId = deviceId; + if (_tag != null) + { + if (deviceId == null) + throw new ArgumentNullException(deviceId, "当 tag 不为空时,必须传入当前设备不变的唯一 id(deviceId)"); + } + + if (string.IsNullOrEmpty(clientId)) throw new Exception("当前 ClientId 为空,无法登录服务器。"); + + return client; + } + + internal void AfterLogIn(AVIMClient client) + { + if (clients == null) clients = new Dictionary(); + client.OnSessionClosed += (sender, e) => { + string clientId = (sender as AVIMClient).ClientId; + clients.Remove(clientId); + if (clients.Count == 0) { + LogOut(); + } + }; + clients[client.ClientId] = client; + } + + #endregion + + #region after-login + + + #endregion + + /// + /// 创建 Client + /// + /// + /// + /// 设备唯一的 Id。如果是 iOS 设备,需要将 iOS 推送使用的 DeviceToken 作为 deviceId 传入 + /// 是否强制加密 wss 链接 + /// + /// + [Obsolete("CreateClient is deprecated, please use CreateClientAsync instead.")] + public Task CreateClient( + string clientId, + string tag = null, + string deviceId = null, + bool secure = true, + CancellationToken cancellationToken = default(CancellationToken)) + { + return this.CreateClientAsync(clientId, tag, deviceId, secure, cancellationToken); + } + + private bool _listening = false; + /// + /// websocket 事件的监听的开关 + /// + /// 是否打开 + public void ToggleNotification(bool toggle) + { + AVRealtime.PrintLog("ToggleNotification| toggle:" + toggle + "|listening: " + _listening); + if (toggle && !_listening) + { + AVWebSocketClient.OnClosed += WebsocketClient_OnClosed; + AVWebSocketClient.OnMessage += WebSocketClient_OnMessage; + _listening = true; + } + else if (!toggle && _listening) + { + AVWebSocketClient.OnClosed -= WebsocketClient_OnClosed; + AVWebSocketClient.OnMessage -= WebSocketClient_OnMessage; + _listening = false; + } + } + + //public void ToggleOfflineNotification(bool toggle) + //{ + // if (toggle) + // { + // PCLWebsocketClient.OnMessage += WebSocketClient_OnMessage_On_Session_Opening; + // } + // else + // { + // PCLWebsocketClient.OnMessage -= WebSocketClient_OnMessage_On_Session_Opening; + // } + //} + + //private void WebSocketClient_OnMessage_On_Session_Opening(string obj) + //{ + // AVRealtime.PrintLog("offline<=" + obj); + //} + + + string _beatPacket = "{}"; + bool _heartBeatingToggle = true; + IAVTimer _heartBeatingTimer; + /// + /// 主动发送心跳包 + /// + /// 是否开启 + /// 时间间隔 + /// 心跳包的内容,默认是个空的 {} + public void ToggleHeartBeating(bool toggle = true, double interval = 60000, string beatPacket = "{}") + { + this._heartBeatingToggle = toggle; + if (!string.Equals(_beatPacket, beatPacket)) _beatPacket = beatPacket; + + if (_heartBeatingTimer == null && this._heartBeatingToggle) + { + _heartBeatingTimer = new AVTimer(); + _heartBeatingTimer.Elapsed += SendHeartBeatingPacket; + _heartBeatingTimer.Interval = interval; + _heartBeatingTimer.Start(); + PrintLog("auto heart beating started."); + } + if (!this._heartBeatingToggle && _heartBeatingTimer != null) + { + _heartBeatingTimer.Stop(); + _heartBeatingTimer = null; + } + } + + void SendHeartBeatingPacket(object sender, TimerEventArgs e) + { + PrintLog("auto heart beating ticked by timer."); +#if MONO || UNITY + Dispatcher.Instance.Post(() => + { + KeepAlive(); + }); +#else + KeepAlive(); +#endif + } + + /// + /// Keeps the alive. + /// + public void KeepAlive() + { + try + { + var cmd = new AVIMCommand(); + RunCommandAsync(cmd).ContinueWith(t => + { + if (t.IsCanceled || t.IsFaulted || t.Exception != null) + { + InvokeNetworkState(); + } + }); + } + catch (Exception) + { + InvokeNetworkState(); + } + } + + internal bool sessionConflict = false; + internal bool loggedOut = false; + + internal bool CanReconnect + { + get + { + return !sessionConflict && !loggedOut && state == Status.Offline; + } + } + + /// + /// 开始自动重连 + /// + public void StartAutoReconnect() + { + + } + internal bool useSecondary = false; + internal bool reborn = false; + + internal Task LogInAsync(string clientId, + string tag = null, + string deviceId = null, + bool secure = true, + CancellationToken cancellationToken = default(CancellationToken)) + { + lock (mutex) + { + var cmd = new SessionCommand() + .UA(VersionString) + .Tag(tag) + .DeviceId(deviceId) + .Option("open") + .PeerId(clientId); + + var result = AttachSignature(cmd, this.SignatureFactory.CreateConnectSignature(clientId)).OnSuccess(_ => + { + return RunCommandAsync(cmd); + }).Unwrap().OnSuccess(t => + { + AVRealtime.PrintLog("sesstion opened."); + if (t.Exception != null) + { + var imException = t.Exception.InnerException as AVIMException; + throw imException; + } + state = Status.Online; + var response = t.Result.Item2; + if (response.ContainsKey("st")) + { + _sesstionToken = response["st"] as string; + } + if (response.ContainsKey("stTtl")) + { + var stTtl = long.Parse(response["stTtl"].ToString()); + _sesstionTokenExpire = DateTime.Now.ToUnixTimeStamp() + stTtl * 1000; + } + return t.Result; + }); + + return result; + } + + } + + internal Task OpenSessionAsync(string clientId, + string tag = null, + string deviceId = null, + string nonce = null, + long timestamp = 0, + string signature = null, + bool secure = true) + { + var cmd = new SessionCommand() + .UA(VersionString) + .Tag(tag) + .DeviceId(deviceId) + .Option("open") + .PeerId(clientId) + .Argument("n", nonce) + .Argument("t", timestamp) + .Argument("s", signature); + + return RunCommandAsync(cmd).OnSuccess(t => + { + AVRealtime.PrintLog("sesstion opened."); + if (t.Exception != null) + { + var imException = t.Exception.InnerException as AVIMException; + throw imException; + } + state = Status.Online; + var response = t.Result.Item2; + if (response.ContainsKey("st")) + { + _sesstionToken = response["st"] as string; + } + if (response.ContainsKey("stTtl")) + { + var stTtl = long.Parse(response["stTtl"].ToString()); + _sesstionTokenExpire = DateTime.Now.ToUnixTimeStamp() + stTtl * 1000; + } + return t.Result; + }); + + } + + /// + /// 自动重连 + /// + /// + Task AutoReconnect() + { + AVRealtime.PrintLog("AutoReconnect started."); + var reconnectingArgs = new AVIMReconnectingEventArgs() + { + ClientId = _clientId, + IsAuto = true, + SessionToken = _sesstionToken + }; + m_OnReconnecting?.Invoke(this, reconnectingArgs); + + var tcs = new TaskCompletionSource(); + Task task; + if (reborn) + { + AVRealtime.PrintLog("both preferred and secondary websockets are expired, so try to request RTM router to get a new pair"); + task = OpenAsync(this._secure, Subprotocol, true); + } else { + var websocketServer = _wss; + if (useSecondary) { + AVRealtime.PrintLog(string.Format("preferred websocket server ({0}) network broken, take secondary server({1}) :", _wss, _secondaryWss)); + websocketServer = _secondaryWss; + } + task = OpenAsync(websocketServer, Subprotocol, true); + } + + task.ContinueWith(t => + { + if (t.IsFaulted || t.IsCanceled) { + state = Status.Reconnecting; + var reconnectFailedArgs = new AVIMReconnectFailedArgs() { + ClientId = _clientId, + IsAuto = true, + SessionToken = _sesstionToken, + FailedCode = 0// network broken. + }; + m_OnReconnectFailed?.Invoke(this, reconnectFailedArgs); + state = Status.Offline; + tcs.SetException(t.Exception); + throw t.Exception; + } else { + state = Status.Opened; + SetNetworkState(); + + void onClose(int code, string reason, string detail) { + AVRealtime.PrintLog("disconnect when open session"); + var ex = new Exception("connection is closed"); + tcs.SetException(ex); + AVWebSocketClient.OnClosed -= onClose; + throw ex; + }; + AVWebSocketClient.OnClosed += onClose; + + if (this.IsSesstionTokenExpired) { + AVRealtime.PrintLog("session is expired, auto relogin with clientId :" + _clientId); + return this.LogInAsync(_clientId, this._tag, this._deviceId, this._secure).ContinueWith(o => { + AVWebSocketClient.OnClosed -= onClose; + return !o.IsFaulted; + }); + } else { + var sessionCMD = new SessionCommand().UA(VersionString).R(1); + + if (string.IsNullOrEmpty(_tag)) { + sessionCMD = sessionCMD.Tag(_tag).SessionToken(this._sesstionToken); + } + + var cmd = sessionCMD.Option("open") + .PeerId(_clientId); + + AVRealtime.PrintLog("reopen session with session token :" + _sesstionToken); + return RunCommandAsync(cmd).ContinueWith(o => { + AVWebSocketClient.OnClosed -= onClose; + return !o.IsFaulted; + }); + } + } + }).Unwrap().ContinueWith(s => + { + if (s.IsFaulted || s.Exception != null) + { + var reconnectFailedArgs = new AVIMReconnectFailedArgs() + { + ClientId = _clientId, + IsAuto = true, + SessionToken = _sesstionToken, + FailedCode = 1 + }; + m_OnReconnectFailed?.Invoke(this, reconnectFailedArgs); + state = Status.Offline; + tcs.SetException(s.Exception); + } + else + { + var reconnectedArgs = new AVIMReconnectedEventArgs() { + ClientId = _clientId, + IsAuto = true, + SessionToken = _sesstionToken, + }; + state = Status.Online; + m_OnReconnected?.Invoke(this, reconnectedArgs); + ToggleNotification(true); + ToggleHeartBeating(true); + tcs.SetResult(true); + } + }); + + return tcs.Task; + } + + + + #region register IAVIMMessage + /// + /// Registers the subtype of the message. + /// + /// The 1st type parameter. + public void RegisterMessageType() where T : IAVIMMessage + { + AVIMCorePlugins.Instance.FreeStyleClassingController.RegisterSubclass(typeof(T)); + } + #endregion + + /// + /// open websocket with default configurations. + /// + /// + public Task OpenAsync(bool secure = true) + { + return this.OpenAsync(secure, null); + } + + /// + /// Open websocket connection. + /// + /// The async. + /// If set to true secure. + /// Subprotocol. + /// If set to true enforce. + /// Cancellation token. + public Task OpenAsync(bool secure, string subprotocol = null, bool enforce = false, CancellationToken cancellationToken = default(CancellationToken)) + { + _secure = secure; + if (state == Status.Online && !enforce) + { + AVRealtime.PrintLog("state is Status.Online."); + return Task.FromResult(true); + } + + if (CurrentConfiguration.RealtimeServer != null) + { + _wss = CurrentConfiguration.RealtimeServer.ToString(); + AVRealtime.PrintLog("use configuration realtime server with url: " + _wss); + return OpenAsync(_wss, subprotocol, enforce); + } + var routerUrl = CurrentConfiguration.RTMRouter != null ? CurrentConfiguration.RTMRouter.ToString() : null; + return RouterController.GetAsync(routerUrl, secure, cancellationToken).OnSuccess(r => + { + var routerState = r.Result; + if (routerState == null) + { + return Task.FromResult(false); + } + _wss = routerState.server; + _secondaryWss = routerState.secondary; + state = Status.Connecting; + AVRealtime.PrintLog("push router give a url :" + _wss); + return OpenAsync(routerState.server, subprotocol, enforce); + }).Unwrap(); + } + + /// + /// open webcoket connection with cloud. + /// + /// wss address + /// subprotocol for websocket + /// + /// + public Task OpenAsync(string url, string subprotocol = null, bool enforce = false, CancellationToken cancellationToken = default(CancellationToken)) + { + if (AVWebSocketClient.IsOpen && !enforce) + { + AVRealtime.PrintLog(url + "is already connectd."); + return Task.FromResult(true); + } + + AVRealtime.PrintLog("websocket try to connect url :" + url + " with subprotocol: " + subprotocol); + AVRealtime.PrintLog(url + " \tconnecting..."); + + return AVWebSocketClient.Connect(url, subprotocol); + } + + /// + /// send websocket command to Realtime server. + /// + /// + /// + public Task>> RunCommandAsync(AVIMCommand command) + { + command.AppId(this.CurrentConfiguration.ApplicationId); + return this.AVIMCommandRunner.RunCommandAsync(command); + } + + /// + /// + /// + /// + public void RunCommand(AVIMCommand command) + { + command.AppId(this.CurrentConfiguration.ApplicationId); + this.AVIMCommandRunner.RunCommand(command); + } + + internal Task AttachSignature(AVIMCommand command, Task SignatureTask) + { + AVRealtime.PrintLog("begin to attach singature."); + var tcs = new TaskCompletionSource(); + if (SignatureTask == null) + { + tcs.SetResult(command); + return tcs.Task; + } + return SignatureTask.OnSuccess(_ => + { + if (_.Result != null) + { + var signature = _.Result; + command.Argument("t", signature.Timestamp); + command.Argument("n", signature.Nonce); + command.Argument("s", signature.SignatureContent); + AVRealtime.PrintLog("AttachSignature ended.t:" + signature.Timestamp + ";n:" + signature.Nonce + ";s:" + signature.SignatureContent); + } + return command; + }); + } + + #region log out and clean event subscribtion + private void WebsocketClient_OnClosed(int errorCode, string reason, string detail) + { + PrintLog(string.Format("websocket closed with code is {0},reason is {1} and detail is {2}", errorCode, reason, detail)); + state = Status.Offline; + + ToggleNotification(false); + ToggleHeartBeating(false); + + var disconnectEventArgs = new AVIMDisconnectEventArgs(errorCode, reason, detail); + m_OnDisconnected?.Invoke(this, disconnectEventArgs); + + this.WebSocketState = new WebSocketStateOptions() + { + ClosedCode = errorCode + }; + PrepareReconnect(); + } + + private void WebsocketClient_OnError(string obj) + { + PrintLog("error:" + obj); + // 如果遇到 WebSocket 错误之后,先关闭,再按断线处理 + AVWebSocketClient.Close(); + WebsocketClient_OnClosed(0, obj, string.Empty); + } + + void PrepareReconnect() { + AVRealtime.PrintLog("Prepare Reconnect"); + Task.Delay(RECONNECT_DELAY).ContinueWith(_ => { + // 开启重连 + AutoReconnect().ContinueWith(t => { + if (t.IsFaulted) { + // 重连失败,延迟再次重连 + reconnectTimes++; + AVRealtime.PrintLog(String.Format("reconnect {0} times", reconnectTimes)); + if (reconnectTimes >= RECONNECT_FROM_APP_ROUTER) { + // 如果大于当前服务地址的最大重连次数,则清空 Router 后重新重连 + RouterController.ClearCache().ContinueWith(__ => { + reborn = true; + PrepareReconnect(); + }); + + } else if (reconnectTimes >= RECONNECT_USE_SECONDARY_TIMES) { + // 如果大于单台 IM 服务器的重连次数,则启用备用服务器 + useSecondary = true; + PrepareReconnect(); + } else { + PrepareReconnect(); + } + } else { + // 重连成功 + reconnectTimes = 0; + reborn = false; + useSecondary = false; + } + }); + }); + } + + internal void LogOut() + { + State = Status.Closed; + loggedOut = true; + Dispose(); + AVWebSocketClient.Close(); + } + + internal void Dispose() + { + var toggle = false; + ToggleNotification(toggle); + ToggleHeartBeating(toggle); + + if (m_NoticeReceived != null) + { + foreach (Delegate d in m_NoticeReceived.GetInvocationList()) + { + m_NoticeReceived -= (EventHandler)d; + } + } + if (m_OnDisconnected != null) + { + foreach (Delegate d in m_OnDisconnected.GetInvocationList()) + { + m_OnDisconnected -= (EventHandler)d; + } + } + } + #endregion + + static AVRealtime() + { +#if MONO || UNITY + versionString = "net-unity/" + Version; +#else + versionString = "net-universal/" + Version; +#endif + } + + private static readonly string versionString; + internal static string VersionString + { + get + { + return versionString; + } + } + + internal static System.Version Version + { + get + { + AssemblyName assemblyName = new AssemblyName(typeof(AVRealtime).GetTypeInfo().Assembly.FullName); + return assemblyName.Version; + } + } + } +} diff --git a/RTM/Source/Public/IAVIMListener.cs b/RTM/Source/Public/IAVIMListener.cs new file mode 100644 index 0000000..366f4a2 --- /dev/null +++ b/RTM/Source/Public/IAVIMListener.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LeanCloud.Realtime +{ + /// + /// WebSocket 监听服务端事件通知的接口 + /// 所有基于协议层的事件监听都需要实现这个接口,然后自定义监听协议。 + /// + public interface IAVIMListener + { + /// + /// 监听的协议 Hook + /// 例如,消息的协议是 direct 命令,因此消息监听需要判断 == "direct" 才可以调用 + /// + /// + /// + bool ProtocolHook(AVIMNotice notice); + + ///// + ///// 如果 返回 true,则会启动 NoticeAction 里面的回调逻辑 + ///// + //Action NoticeAction { get; set; } + + /// + /// 如果 返回 true,则会启动 NoticeAction 里面的回调逻辑 + /// + void OnNoticeReceived(AVIMNotice notice); + } +} diff --git a/RTM/Source/Public/IAVIMMessage.cs b/RTM/Source/Public/IAVIMMessage.cs new file mode 100644 index 0000000..f797485 --- /dev/null +++ b/RTM/Source/Public/IAVIMMessage.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LeanCloud.Realtime +{ + /// + /// 消息接口 + /// 所有消息必须实现这个接口 + /// + public interface IAVIMMessage + { + /// + /// Serialize this instance. + /// + /// The serialize. + string Serialize(); + + /// + /// Validate the specified msgStr. + /// + /// The validate. + /// Message string. + bool Validate(string msgStr); + + /// + /// Deserialize the specified msgStr. + /// + /// The deserialize. + /// Message string. + IAVIMMessage Deserialize(string msgStr); + + /// + /// Gets or sets the conversation identifier. + /// + /// The conversation identifier. + string ConversationId { get; set; } + + /// + /// Gets or sets from client identifier. + /// + /// From client identifier. + string FromClientId { get; set; } + + /// + /// Gets or sets the identifier. + /// + /// The identifier. + string Id { get; set; } + + /// + /// Gets or sets the server timestamp. + /// + /// The server timestamp. + long ServerTimestamp { get; set; } + + /// + /// Gets or sets the rcp timestamp. + /// + /// The rcp timestamp. + long RcpTimestamp { get; set; } + + long UpdatedAt { get; set; } + + + #region mention features. + /// + /// Gets or sets a value indicating whether this mention all. + /// + /// true if mention all; otherwise, false. + bool MentionAll { get; set; } + + /// + /// Gets or sets the mention list. + /// + /// The mention list. + IEnumerable MentionList { get; set; } + #endregion + + } +} diff --git a/RTM/Source/Public/ICacheEngine.cs b/RTM/Source/Public/ICacheEngine.cs new file mode 100644 index 0000000..1c20ec7 --- /dev/null +++ b/RTM/Source/Public/ICacheEngine.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LeanCloud.Realtime +{ + public interface ISQLStorage + { + + } +} diff --git a/RTM/Source/Public/ISignatureFactory.cs b/RTM/Source/Public/ISignatureFactory.cs new file mode 100644 index 0000000..ffda11a --- /dev/null +++ b/RTM/Source/Public/ISignatureFactory.cs @@ -0,0 +1,131 @@ +using LeanCloud; +using LeanCloud.Storage.Internal; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + + +namespace LeanCloud.Realtime +{ + /// + /// 对话操作的签名类型,比如讲一个 client id 加入到对话中 + /// + /// + public enum ConversationSignatureAction + { + /// + /// add 加入对话和邀请对方加入对话 + /// + Add, + /// + /// remove 当前 client Id 离开对话和将其他人踢出对话 + /// + Remove + } + + /// + /// + /// + public interface ISignatureFactory + { + + /// + /// 构建登录签名 + /// + /// 需要登录到云端服务器的 client Id + /// + Task CreateConnectSignature(string clientId); + + /// + /// + /// + /// + /// + /// + Task CreateStartConversationSignature(string clientId, IEnumerable targetIds); + + /// + /// + /// + /// + /// + /// + /// 需要签名的操作 + /// + Task CreateConversationSignature(string conversationId, string clientId, IEnumerable targetIds, ConversationSignatureAction action); + } + + internal class DefaulSiganatureFactory : ISignatureFactory + { + Task ISignatureFactory.CreateConnectSignature(string clientId) + { + return Task.FromResult(null); + } + + Task ISignatureFactory.CreateConversationSignature(string conversationId, string clientId, IEnumerable targetIds, ConversationSignatureAction action) + { + return Task.FromResult(null); + } + + Task ISignatureFactory.CreateStartConversationSignature(string clientId, IEnumerable targetIds) + { + return Task.FromResult(null); + } + } + + public class LeanEngineSignatureFactory : ISignatureFactory + { + public Task CreateConnectSignature(string clientId) + { + var data = new Dictionary(); + data.Add("client_id", clientId); + return AVCloud.CallFunctionAsync>("connect", data).OnSuccess(_ => + { + var jsonData = _.Result; + var s = jsonData["signature"].ToString(); + var n = jsonData["nonce"].ToString(); + var t = long.Parse(jsonData["timestamp"].ToString()); + var signature = new AVIMSignature(s, t, n); + return signature; + }); + } + + public Task CreateStartConversationSignature(string clientId, IEnumerable targetIds) + { + var data = new Dictionary(); + data.Add("client_id", clientId); + data.Add("members", targetIds.ToList()); + return AVCloud.CallFunctionAsync>("startConversation", data).OnSuccess(_ => + { + var jsonData = _.Result; + var s = jsonData["signature"].ToString(); + var n = jsonData["nonce"].ToString(); + var t = long.Parse(jsonData["timestamp"].ToString()); + var signature = new AVIMSignature(s, t, n); + return signature; + }); + } + + public Task CreateConversationSignature(string conversationId, string clientId, IEnumerable targetIds, ConversationSignatureAction action) + { + var actionList = new string[] { "invite", "kick" }; + var data = new Dictionary(); + data.Add("client_id", clientId); + data.Add("conv_id", conversationId); + data.Add("members", targetIds.ToList()); + data.Add("action", actionList[(int)action]); + return AVCloud.CallFunctionAsync>("oprateConversation", data).OnSuccess(_ => + { + var jsonData = _.Result; + var s = jsonData["signature"].ToString(); + var n = jsonData["nonce"].ToString(); + var t = long.Parse(jsonData["timestamp"].ToString()); + var signature = new AVIMSignature(s, t, n); + return signature; + }); + } + + } +} diff --git a/RTM/Source/Public/Listener/AVIMConversationListener.cs b/RTM/Source/Public/Listener/AVIMConversationListener.cs new file mode 100644 index 0000000..77f931a --- /dev/null +++ b/RTM/Source/Public/Listener/AVIMConversationListener.cs @@ -0,0 +1,256 @@ +using LeanCloud.Storage.Internal; +using LeanCloud.Realtime.Internal; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LeanCloud.Realtime +{ + /// + /// 对话中成员变动的事件参数,它提供被操作的对话(Conversation),操作类型(AffectedType) + /// 受影响的成员列表(AffectedMembers) + /// + public class AVIMOnMembersChangedEventArgs : EventArgs + { + /// + /// 本次成员变动中被操作的具体对话(AVIMConversation)的对象 + /// + public AVIMConversation Conversation { get; set; } + + /// + /// 变动的类型 + /// + public AVIMConversationEventType AffectedType { get; internal set; } + + /// + /// 受影响的成员的 Client Ids + /// + public IList AffectedMembers { get; set; } + + /// + /// 操作人的 Client ClientId + /// + public string Oprator { get; set; } + + /// + /// 操作的时间,已转化为本地时间 + /// + public DateTime OpratedTime { get; set; } + } + + /// + /// 变动的类型,目前支持如下: + /// 1、Joined:当前 Client 主动加入,案例:当 A 主动加入到对话,A 将收到 Joined 事件响应,其余的成员收到 MembersJoined 事件响应 + /// 2、Left:当前 Client 主动退出,案例:当 A 从对话中退出,A 将收到 Left 事件响应,其余的成员收到 MembersLeft 事件响应 + /// 3、MembersJoined:某个成员加入(区别于Joined和Kicked),案例:当 A 把 B 加入到对话中,C 将收到 MembersJoined 事件响应 + /// 4、MembersLeft:某个成员加入(区别于Joined和Kicked),案例:当 A 把 B 从对话中剔除,C 将收到 MembersLeft 事件响应 + /// 5、Invited:当前 Client 被邀请加入,案例:当 A 被 B 邀请加入到对话中,A 将收到 Invited 事件响应,B 将收到 Joined ,其余的成员收到 MembersJoined 事件响应 + /// 6、Kicked:当前 Client 被剔除,案例:当 A 被 B 从对话中剔除,A 将收到 Kicked 事件响应,B 将收到 Left,其余的成员收到 MembersLeft 事件响应 + /// + public enum AVIMConversationEventType + { + /// + /// 自身主动加入 + /// + Joined = 1, + /// + /// 自身主动离开 + /// + Left, + /// + /// 他人加入 + /// + MembersJoined, + /// + /// 他人离开 + /// + MembersLeft, + /// + /// 自身被邀请加入 + /// + Invited, + /// + /// 自身被他人剔除 + /// + Kicked + } + + #region AVIMMembersJoinListener + //when Members joined or invited by member,this listener will invoke AVIMOnMembersJoinedEventArgs event. + /// + /// 对话中有成员加入的时候,在改对话中的其他成员都会触发 事件 + /// + public class AVIMMembersJoinListener : IAVIMListener + { + + private EventHandler m_OnMembersJoined; + /// + /// 有成员加入到对话时,触发的事件 + /// + public event EventHandler OnMembersJoined + { + add + { + m_OnMembersJoined += value; + } + remove + { + m_OnMembersJoined -= value; + } + } + + public virtual void OnNoticeReceived(AVIMNotice notice) + { + if (m_OnMembersJoined != null) + { + var joinedMembers = AVDecoder.Instance.DecodeList(notice.RawData["m"]); + var ivitedBy = notice.RawData["initBy"].ToString(); + var conersationId = notice.RawData["cid"].ToString(); + var args = new AVIMOnMembersJoinedEventArgs() + { + ConversationId = conersationId, + InvitedBy = ivitedBy, + JoinedMembers = joinedMembers + }; + m_OnMembersJoined.Invoke(this, args); + } + } + + public virtual bool ProtocolHook(AVIMNotice notice) + { + if (notice.CommandName != "conv") return false; + if (!notice.RawData.ContainsKey("op")) return false; + var op = notice.RawData["op"].ToString(); + if (!op.Equals("members-joined")) return false; + return true; + } + } + #endregion + + #region AVIMMembersLeftListener + // when Members left or kicked by member,this listener will invoke AVIMOnMembersJoinedEventArgs event. + /// + /// 对话中有成员加入的时候,在改对话中的其他成员都会触发 OnMembersJoined 事件 + /// + public class AVIMMembersLeftListener : IAVIMListener + { + private EventHandler m_OnMembersLeft; + /// + /// 有成员加入到对话时,触发的事件 + /// + public event EventHandler OnMembersLeft + { + add + { + m_OnMembersLeft += value; + } + remove + { + m_OnMembersLeft -= value; + } + } + public virtual void OnNoticeReceived(AVIMNotice notice) + { + if (m_OnMembersLeft != null) + { + var leftMembers = AVDecoder.Instance.DecodeList(notice.RawData["m"]); + var kickedBy = notice.RawData["initBy"].ToString(); + var conersationId = notice.RawData["cid"].ToString(); + var args = new AVIMOnMembersLeftEventArgs() + { + ConversationId = conersationId, + KickedBy = kickedBy, + LeftMembers = leftMembers + }; + m_OnMembersLeft.Invoke(this, args); + } + } + + public virtual bool ProtocolHook(AVIMNotice notice) + { + if (notice.CommandName != "conv") return false; + if (!notice.RawData.ContainsKey("op")) return false; + var op = notice.RawData["op"].ToString(); + if (!op.Equals("members-left")) return false; + return true; + } + } + #endregion + + #region AVIMInvitedListener + public class AVIMInvitedListener : IAVIMListener + { + private EventHandler m_OnInvited; + public event EventHandler OnInvited { + add { + m_OnInvited += value; + } remove { + m_OnInvited -= value; + } + } + public void OnNoticeReceived(AVIMNotice notice) + { + if (m_OnInvited != null) + { + var ivitedBy = notice.RawData["initBy"].ToString(); + var conersationId = notice.RawData["cid"].ToString(); + var args = new AVIMOnInvitedEventArgs() + { + ConversationId = conersationId, + InvitedBy = ivitedBy, + }; + m_OnInvited.Invoke(this, args); + } + } + + public bool ProtocolHook(AVIMNotice notice) + { + if (notice.CommandName != "conv") return false; + if (!notice.RawData.ContainsKey("op")) return false; + var op = notice.RawData["op"].ToString(); + if (!op.Equals("joined")) return false; + return true; + } + } + #endregion + + #region AVIMKickedListener + public class AVIMKickedListener : IAVIMListener + { + private EventHandler m_OnKicked; + public event EventHandler OnKicked { + add { + m_OnKicked += value; + } remove { + m_OnKicked -= value; + } + } + public void OnNoticeReceived(AVIMNotice notice) + { + if (m_OnKicked != null) + { + var kickcdBy = notice.RawData["initBy"].ToString(); + var conersationId = notice.RawData["cid"].ToString(); + var args = new AVIMOnKickedEventArgs() + { + ConversationId = conersationId, + KickedBy = kickcdBy, + }; + m_OnKicked.Invoke(this, args); + } + } + + public bool ProtocolHook(AVIMNotice notice) + { + if (notice.CommandName != "conv") return false; + if (!notice.RawData.ContainsKey("op")) return false; + var op = notice.RawData["op"].ToString(); + if (!op.Equals("left")) return false; + return true; + } + } + #endregion + +} diff --git a/RTM/Source/Public/Listener/ConversationUnreadListener.cs b/RTM/Source/Public/Listener/ConversationUnreadListener.cs new file mode 100644 index 0000000..bd48b89 --- /dev/null +++ b/RTM/Source/Public/Listener/ConversationUnreadListener.cs @@ -0,0 +1,145 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using LeanCloud.Realtime.Internal; + +namespace LeanCloud.Realtime +{ + internal class ConversationUnreadListener : IAVIMListener + { + internal class UnreadConversationNotice : IEqualityComparer + { + internal readonly object mutex = new object(); + internal IAVIMMessage LastUnreadMessage { get; set; } + internal string ConvId { get; set; } + internal int UnreadCount { get; set; } + + public bool Equals(UnreadConversationNotice x, UnreadConversationNotice y) + { + return x.ConvId == y.ConvId; + } + + public int GetHashCode(UnreadConversationNotice obj) + { + return obj.ConvId.GetHashCode(); + } + + internal void AutomicIncrement() + { + lock (mutex) + { + UnreadCount++; + } + } + } + internal static readonly object sMutex = new object(); + internal static long NotifTime; + internal static HashSet UnreadConversations; + static ConversationUnreadListener() + { + UnreadConversations = new HashSet(new UnreadConversationNotice()); + NotifTime = DateTime.Now.ToUnixTimeStamp(); + } + + internal static void UpdateNotice(IAVIMMessage message) + { + lock (sMutex) + { + var convValidators = UnreadConversations.Where(c => c.ConvId == message.ConversationId); + if (convValidators != null) + { + if (convValidators.Count() > 0) + { + var currentNotice = convValidators.FirstOrDefault(); + currentNotice.AutomicIncrement(); + currentNotice.LastUnreadMessage = message; + } + else + { + var currentThread = new UnreadConversationNotice(); + currentThread.ConvId = message.ConversationId; + currentThread.LastUnreadMessage = message; + currentThread.AutomicIncrement(); + UnreadConversations.Add(currentThread); + } + } + } + } + internal static void ClearUnread(string convId) + { + UnreadConversations.Remove(Get(convId)); + } + internal static IEnumerable FindAllConvIds() + { + lock (sMutex) + { + return ConversationUnreadListener.UnreadConversations.Select(c => c.ConvId); + } + } + + internal static UnreadConversationNotice Get(string convId) + { + lock (sMutex) + { + var unreadValidator = ConversationUnreadListener.UnreadConversations.Where(c => c.ConvId == convId); + if (unreadValidator != null) + { + if (unreadValidator.Count() > 0) + { + var notice = unreadValidator.FirstOrDefault(); + return notice; + } + } + return null; + } + } + + public void OnNoticeReceived(AVIMNotice notice) + { + lock (sMutex) + { + if (notice.RawData.ContainsKey("convs")) + { + var unreadRawData = notice.RawData["convs"] as List; + if (notice.RawData.ContainsKey("notifTime")) + { + long.TryParse(notice.RawData["notifTime"].ToString(), out NotifTime); + } + foreach (var data in unreadRawData) + { + var dataMap = data as IDictionary; + if (dataMap != null) + { + var convId = dataMap["cid"].ToString(); + var ucn = Get(convId); + if (ucn == null) ucn = new UnreadConversationNotice(); + + ucn.ConvId = convId; + var unreadCount = 0; + Int32.TryParse(dataMap["unread"].ToString(), out unreadCount); + ucn.UnreadCount = unreadCount; + + #region restore last message for the conversation + if (dataMap.ContainsKey("data")) + { + var msgStr = dataMap["data"].ToString(); + var messageObj = AVRealtime.FreeStyleMessageClassingController.Instantiate(msgStr, dataMap); + ucn.LastUnreadMessage = messageObj; + } + + UnreadConversations.Add(ucn); + #endregion + } + } + } + } + } + + public bool ProtocolHook(AVIMNotice notice) + { + return notice.CommandName == "unread"; + } + } +} diff --git a/RTM/Source/Public/Listener/GoAwayListener.cs b/RTM/Source/Public/Listener/GoAwayListener.cs new file mode 100644 index 0000000..57ecb62 --- /dev/null +++ b/RTM/Source/Public/Listener/GoAwayListener.cs @@ -0,0 +1,28 @@ +using System; + +namespace LeanCloud.Realtime { + /// + /// 强制被踢下线处理 + /// + internal class GoAwayListener : IAVIMListener { + Action onGoAway; + + public event Action OnGoAway { + add { + onGoAway += value; + } + remove { + onGoAway -= value; + } + } + + public void OnNoticeReceived(AVIMNotice notice) { + // TODO 退出并清理路由缓存 + onGoAway?.Invoke(); + } + + public bool ProtocolHook(AVIMNotice notice) { + return notice.CommandName == "goaway"; + } + } +} diff --git a/RTM/Source/Public/Listener/MessagePatchListener.cs b/RTM/Source/Public/Listener/MessagePatchListener.cs new file mode 100644 index 0000000..b588190 --- /dev/null +++ b/RTM/Source/Public/Listener/MessagePatchListener.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LeanCloud.Realtime +{ + internal delegate void OnMessagePatch(IEnumerable messages); + internal class MessagePatchListener : IAVIMListener + { + public OnMessagePatch OnReceived { get; set; } + + public void OnNoticeReceived(AVIMNotice notice) + { + ICollection patchedMessages = new List(); + var msgObjs = notice.RawData["patches"] as IList; + if (msgObjs != null) + { + foreach (var msgObj in msgObjs) + { + var msgData = msgObj as IDictionary; + if (msgData != null) + { + var msgStr = msgData["data"] as string; + var message = AVRealtime.FreeStyleMessageClassingController.Instantiate(msgStr, msgData); + patchedMessages.Add(message); + } + } + } + if (OnReceived != null) + { + if (patchedMessages.Count > 0) + { + this.OnReceived(patchedMessages); + } + } + } + + public bool ProtocolHook(AVIMNotice notice) + { + if (notice.CommandName != "patch") return false; + if (!notice.RawData.ContainsKey("op")) return false; + if (notice.RawData["op"].ToString() != "modify") return false; + return true; + } + } +} diff --git a/RTM/Source/Public/Listener/OfflineMessageListener.cs b/RTM/Source/Public/Listener/OfflineMessageListener.cs new file mode 100644 index 0000000..983ad8a --- /dev/null +++ b/RTM/Source/Public/Listener/OfflineMessageListener.cs @@ -0,0 +1,42 @@ +using LeanCloud.Storage.Internal; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace LeanCloud.Realtime +{ + internal class OfflineMessageListener : IAVIMListener + { + private EventHandler m_OnOfflineMessageReceived; + public event EventHandler OnOfflineMessageReceived + { + add + { + m_OnOfflineMessageReceived += value; + } + remove + { + m_OnOfflineMessageReceived -= value; + } + } + public void OnNoticeReceived(AVIMNotice notice) + { + if (m_OnOfflineMessageReceived != null) + { + var msgStr = notice.RawData["msg"].ToString(); + var iMessage = AVRealtime.FreeStyleMessageClassingController.Instantiate(msgStr, notice.RawData); + var args = new AVIMMessageEventArgs(iMessage); + m_OnOfflineMessageReceived.Invoke(this, args); + } + + } + + public bool ProtocolHook(AVIMNotice notice) + { + if (notice.CommandName != "direct") return false; + if (!notice.RawData.ContainsKey("offline")) return false; + return true; + } + } +} diff --git a/RTM/Source/Public/Listener/SessionListener.cs b/RTM/Source/Public/Listener/SessionListener.cs new file mode 100644 index 0000000..aad4c54 --- /dev/null +++ b/RTM/Source/Public/Listener/SessionListener.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace LeanCloud.Realtime +{ + internal class SessionListener : IAVIMListener + { + private Action _onSessionClosed; + public event Action OnSessionClosed + { + add + { + _onSessionClosed += value; + } + remove + { + _onSessionClosed -= value; + } + } + public void OnNoticeReceived(AVIMNotice notice) + { + var code = 0; + if (notice.RawData.ContainsKey("code")) + { + int.TryParse(notice.RawData["code"].ToString(), out code); + } + + var reason = ""; + if (notice.RawData.ContainsKey("reason")) + { + reason = notice.RawData["reason"].ToString(); + } + + var detail = ""; + if (notice.RawData.ContainsKey("detail")) + { + detail = notice.RawData["detail"].ToString(); + } + + if (_onSessionClosed != null) + { + _onSessionClosed(code, reason, detail); + } + } + + public bool ProtocolHook(AVIMNotice notice) + { + if (notice.CommandName != "session") return false; + if (!notice.RawData.ContainsKey("op")) return false; + if (notice.RawData.ContainsKey("i")) return false; + if (notice.RawData["op"].ToString() != "closed") return false; + + return true; + } + } +} diff --git a/RTM/Source/Public/Unity/AVRealtimeBehavior.cs b/RTM/Source/Public/Unity/AVRealtimeBehavior.cs new file mode 100644 index 0000000..fe84b52 --- /dev/null +++ b/RTM/Source/Public/Unity/AVRealtimeBehavior.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using LeanCloud.Realtime.Internal; +using LeanCloud.Storage.Internal; +using UnityEngine; +using UnityEngine.Networking; + +namespace LeanCloud.Realtime +{ + /// + /// AVRealtime initialize behavior. + /// + public class AVRealtimeBehavior : AVInitializeBehaviour + { + public string RTMRouter = null; + + //void OnApplicationQuit() + //{ + // if (AVRealtime.clients != null) + // { + // foreach (var item in AVRealtime.clients) + // { + // item.Value.LinkedRealtime.LogOut(); + // } + // } + //} + + //private void Update() + //{ + // var available = Application.internetReachability != NetworkReachability.NotReachable; + // if (AVRealtime.clients != null) + // foreach (var item in AVRealtime.clients) + // { + // if (item.Value != null) + // if (item.Value.LinkedRealtime != null) + // item.Value.LinkedRealtime.InvokeNetworkState(available); + // } + //} + + //public override void Awake() + //{ + // base.Awake(); + // StartCoroutine(InitializeRealtime()); + // gameObject.name = "AVRealtimeInitializeBehavior"; + //} + + //public IEnumerator InitializeRealtime() + //{ + // if (isRealtimeInitialized) + // { + // yield break; + // } + // isRealtimeInitialized = true; + // yield return FetchRouter(); + //} + + + //[SerializeField] + //public bool secure; + //private static bool isRealtimeInitialized = false; + //public string Server; + //private IDictionary routerState; + + //public IEnumerator FetchRouter() + //{ + // var router = RTMRouter; + // if (string.IsNullOrEmpty(router)) { + // var state = AVPlugins.Instance.AppRouterController.Get(); + // router = state.RealtimeRouterServer; + // } + // var url = string.Format("https://{0}/v1/route?appId={1}", router, applicationID); + // if (secure) + // { + // url += "&secure=1"; + // } + + // var request = new UnityWebRequest(url); + // request.downloadHandler = new DownloadHandlerBuffer(); + // yield return request.Send(); + + // if (request.isError) + // { + // throw new AVException(AVException.ErrorCode.ConnectionFailed, "can not reach router.", null); + // } + + // var result = request.downloadHandler.text; + // routerState = Json.Parse(result) as IDictionary; + // if (routerState.Keys.Count == 0) + // { + // throw new KeyNotFoundException("Can not get websocket url from server,please check the appId."); + // } + // var ttl = long.Parse(routerState["ttl"].ToString()); + // var expire = DateTime.Now.AddSeconds(ttl); + // routerState["expire"] = expire.ToUnixTimeStamp(UnixTimeStampUnit.Second); + // Server = routerState["server"].ToString(); + //} + + + } +} diff --git a/Storage/Source/Internal/AVCorePlugins.cs b/Storage/Source/Internal/AVCorePlugins.cs new file mode 100644 index 0000000..3b2fd06 --- /dev/null +++ b/Storage/Source/Internal/AVCorePlugins.cs @@ -0,0 +1,384 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using LeanCloud.Storage.Internal; + +namespace LeanCloud.Storage.Internal +{ + public class AVPlugins : IAVCorePlugins + { + private static readonly object instanceMutex = new object(); + private static IAVCorePlugins instance; + public static IAVCorePlugins Instance + { + get + { + lock (instanceMutex) + { + instance = instance ?? new AVPlugins(); + return instance; + } + } + set + { + lock (instanceMutex) + { + instance = value; + } + } + } + + private readonly object mutex = new object(); + + #region Server Controllers + + private IHttpClient httpClient; + private IAppRouterController appRouterController; + private IAVCommandRunner commandRunner; + private IStorageController storageController; + + private IAVCloudCodeController cloudCodeController; + private IAVConfigController configController; + private IAVFileController fileController; + private IAVObjectController objectController; + private IAVQueryController queryController; + private IAVSessionController sessionController; + private IAVUserController userController; + private IObjectSubclassingController subclassingController; + + #endregion + + #region Current Instance Controller + + private IAVCurrentUserController currentUserController; + private IInstallationIdController installationIdController; + + #endregion + + public void Reset() + { + lock (mutex) + { + HttpClient = null; + AppRouterController = null; + CommandRunner = null; + StorageController = null; + + CloudCodeController = null; + FileController = null; + ObjectController = null; + SessionController = null; + UserController = null; + SubclassingController = null; + + CurrentUserController = null; + InstallationIdController = null; + } + } + + public IHttpClient HttpClient + { + get + { + lock (mutex) + { + httpClient = httpClient ?? new HttpClient(); + return httpClient; + } + } + set + { + lock (mutex) + { + httpClient = value; + } + } + } + + public IAppRouterController AppRouterController + { + get + { + lock (mutex) + { + appRouterController = appRouterController ?? new AppRouterController(); + return appRouterController; + } + } + set + { + lock (mutex) + { + appRouterController = value; + } + } + } + + public IAVCommandRunner CommandRunner + { + get + { + lock (mutex) + { + commandRunner = commandRunner ?? new AVCommandRunner(HttpClient, InstallationIdController); + return commandRunner; + } + } + set + { + lock (mutex) + { + commandRunner = value; + } + } + } + +#if !UNITY + public IStorageController StorageController + { + get + { + lock (mutex) + { + storageController = storageController ?? new StorageController(AVClient.CurrentConfiguration.ApplicationId); + return storageController; + } + } + set + { + lock (mutex) + { + storageController = value; + } + } + } +#endif +#if UNITY + public IStorageController StorageController + { + get + { + lock (mutex) + { + storageController = storageController ?? new StorageController(AVInitializeBehaviour.IsWebPlayer, AVClient.CurrentConfiguration.ApplicationId); + return storageController; + } + } + set + { + lock (mutex) + { + storageController = value; + } + } + } +#endif + + public IAVCloudCodeController CloudCodeController + { + get + { + lock (mutex) + { + cloudCodeController = cloudCodeController ?? new AVCloudCodeController(CommandRunner); + return cloudCodeController; + } + } + set + { + lock (mutex) + { + cloudCodeController = value; + } + } + } + + public IAVFileController FileController + { + get + { + lock (mutex) + { + if (AVClient.CurrentConfiguration.RegionValue == 0) + fileController = fileController ?? new QiniuFileController(CommandRunner); + else if (AVClient.CurrentConfiguration.RegionValue == 2) + fileController = fileController ?? new QCloudCosFileController(CommandRunner); + else if (AVClient.CurrentConfiguration.RegionValue == 1) + fileController = fileController ?? new AWSS3FileController(CommandRunner); + + return fileController; + } + } + set + { + lock (mutex) + { + fileController = value; + } + } + } + + public IAVConfigController ConfigController + { + get + { + lock (mutex) + { + if (configController == null) + { + configController = new AVConfigController(CommandRunner, StorageController); + } + return configController; + } + } + set + { + lock (mutex) + { + configController = value; + } + } + } + + public IAVObjectController ObjectController + { + get + { + lock (mutex) + { + objectController = objectController ?? new AVObjectController(CommandRunner); + return objectController; + } + } + set + { + lock (mutex) + { + objectController = value; + } + } + } + + public IAVQueryController QueryController + { + get + { + lock (mutex) + { + if (queryController == null) + { + queryController = new AVQueryController(CommandRunner); + } + return queryController; + } + } + set + { + lock (mutex) + { + queryController = value; + } + } + } + + public IAVSessionController SessionController + { + get + { + lock (mutex) + { + sessionController = sessionController ?? new AVSessionController(CommandRunner); + return sessionController; + } + } + set + { + lock (mutex) + { + sessionController = value; + } + } + } + + public IAVUserController UserController + { + get + { + lock (mutex) + { + userController = userController ?? new AVUserController(CommandRunner); + return userController; + } + } + set + { + lock (mutex) + { + userController = value; + } + } + } + + public IAVCurrentUserController CurrentUserController + { + get + { + lock (mutex) + { + currentUserController = currentUserController ?? new AVCurrentUserController(StorageController); + return currentUserController; + } + } + set + { + lock (mutex) + { + currentUserController = value; + } + } + } + + public IObjectSubclassingController SubclassingController + { + get + { + lock (mutex) + { + if (subclassingController == null) + { + subclassingController = new ObjectSubclassingController(); + subclassingController.AddRegisterHook(typeof(AVUser), () => CurrentUserController.ClearFromMemory()); + } + return subclassingController; + } + } + set + { + lock (mutex) + { + subclassingController = value; + } + } + } + + public IInstallationIdController InstallationIdController + { + get + { + lock (mutex) + { + installationIdController = installationIdController ?? new InstallationIdController(StorageController); + return installationIdController; + } + } + set + { + lock (mutex) + { + installationIdController = value; + } + } + } + } +} diff --git a/Storage/Source/Internal/AppRouter/AppRouterController.cs b/Storage/Source/Internal/AppRouter/AppRouterController.cs new file mode 100644 index 0000000..1e0c73d --- /dev/null +++ b/Storage/Source/Internal/AppRouter/AppRouterController.cs @@ -0,0 +1,103 @@ +using System; +using System.Net; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using LeanCloud.Storage.Internal; + +namespace LeanCloud.Storage.Internal +{ + public class AppRouterController : IAppRouterController + { + private AppRouterState currentState; + private object mutex = new object(); + + /// + /// Get current app's router state + /// + /// + public AppRouterState Get() + { + if (string.IsNullOrEmpty(AVClient.CurrentConfiguration.ApplicationId)) + { + throw new AVException(AVException.ErrorCode.NotInitialized, "ApplicationId can not be null."); + } + AppRouterState state; + state = AppRouterState.GetInitial(AVClient.CurrentConfiguration.ApplicationId, AVClient.CurrentConfiguration.Region); + + lock (mutex) + { + if (currentState != null) + { + if (!currentState.isExpired()) + state = currentState; + } + } + + if (state.isExpired()) + { + lock (mutex) + { + state.FetchedAt = DateTime.Now + TimeSpan.FromMinutes(10); + } + Task.Factory.StartNew(RefreshAsync); + } + return state; + } + + public Task RefreshAsync() + { + return QueryAsync(CancellationToken.None).ContinueWith(t => + { + if (!t.IsFaulted && !t.IsCanceled) + { + lock (mutex) + { + currentState = t.Result; + } + } + }); + } + + public Task QueryAsync(CancellationToken cancellationToken) + { + string appId = AVClient.CurrentConfiguration.ApplicationId; + string url = string.Format("https://app-router.leancloud.cn/2/route?appId={0}", appId); + + return AVClient.HttpGetAsync(new Uri(url)).ContinueWith(t => + { + var tcs = new TaskCompletionSource(); + if (t.Result.Item1 != HttpStatusCode.OK) + { + tcs.SetException(new AVException(AVException.ErrorCode.ConnectionFailed, "can not reach router.", null)); + } + else + { + var result = Json.Parse(t.Result.Item2) as IDictionary; + tcs.SetResult(ParseAppRouterState(result)); + } + return tcs.Task; + }).Unwrap(); + } + + private static AppRouterState ParseAppRouterState(IDictionary jsonObj) + { + var state = new AppRouterState() + { + TTL = (int)jsonObj["ttl"], + StatsServer = jsonObj["stats_server"] as string, + RealtimeRouterServer = jsonObj["rtm_router_server"] as string, + PushServer = jsonObj["push_server"] as string, + EngineServer = jsonObj["engine_server"] as string, + ApiServer = jsonObj["api_server"] as string, + Source = "network", + }; + return state; + } + + public void Clear() + { + currentState = null; + } + } +} diff --git a/Storage/Source/Internal/AppRouter/AppRouterState.cs b/Storage/Source/Internal/AppRouter/AppRouterState.cs new file mode 100644 index 0000000..b2e61f9 --- /dev/null +++ b/Storage/Source/Internal/AppRouter/AppRouterState.cs @@ -0,0 +1,85 @@ +using System; + +namespace LeanCloud.Storage.Internal +{ + public class AppRouterState + { + public long TTL { get; internal set; } + public string ApiServer { get; internal set; } + public string EngineServer { get; internal set; } + public string PushServer { get; internal set; } + public string RealtimeRouterServer { get; internal set; } + public string StatsServer { get; internal set; } + public string Source { get; internal set; } + + public DateTime FetchedAt { get; internal set; } + + private static object mutex = new object(); + + public AppRouterState() + { + FetchedAt = DateTime.Now; + } + + /// + /// Is this app router state expired. + /// + public bool isExpired() + { + return DateTime.Now > FetchedAt + TimeSpan.FromSeconds(TTL); + } + + /// + /// Get the initial usable router state + /// + /// Current app's appId + /// Current app's region + /// Initial app router state + public static AppRouterState GetInitial(string appId, AVClient.Configuration.AVRegion region) + { + var regionValue = (int)region; + var prefix = appId.Substring(0, 8).ToLower(); + switch (regionValue) + { + case 0: + // 华北 + return new AppRouterState() + { + TTL = -1, + ApiServer = String.Format("{0}.api.lncld.net", prefix), + EngineServer = String.Format("{0}.engine.lncld.net", prefix), + PushServer = String.Format("{0}.push.lncld.net", prefix), + RealtimeRouterServer = String.Format("{0}.rtm.lncld.net", prefix), + StatsServer = String.Format("{0}.stats.lncld.net", prefix), + Source = "initial", + }; + case 1: + // 美国 + return new AppRouterState() + { + TTL = -1, + ApiServer = string.Format("{0}.api.lncldglobal.com", prefix), + EngineServer = string.Format("{0}.engine.lncldglobal.com", prefix), + PushServer = string.Format("{0}.push.lncldglobal.com", prefix), + RealtimeRouterServer = string.Format("{0}.rtm.lncldglobal.com", prefix), + StatsServer = string.Format("{0}.stats.lncldglobal.com", prefix), + Source = "initial", + }; + case 2: + // 华东 + return new AppRouterState() { + TTL = -1, + ApiServer = string.Format("{0}.api.lncldapi.com", prefix), + EngineServer = string.Format("{0}.engine.lncldapi.com", prefix), + PushServer = string.Format("{0}.push.lncldapi.com", prefix), + RealtimeRouterServer = string.Format("{0}.rtm.lncldapi.com", prefix), + StatsServer = string.Format("{0}.stats.lncldapi.com", prefix), + Source = "initial", + }; + default: + throw new AVException(AVException.ErrorCode.OtherCause, "invalid region"); + } + } + + } +} \ No newline at end of file diff --git a/Storage/Source/Internal/AppRouter/IAppRouterController.cs b/Storage/Source/Internal/AppRouter/IAppRouterController.cs new file mode 100644 index 0000000..771ee18 --- /dev/null +++ b/Storage/Source/Internal/AppRouter/IAppRouterController.cs @@ -0,0 +1,22 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace LeanCloud.Storage.Internal +{ + public interface IAppRouterController + { + AppRouterState Get(); + /// + /// Start refresh the app router. + /// + /// + Task RefreshAsync(); + void Clear(); + /// + /// Query the app router. + /// + /// New AppRouterState + Task QueryAsync(CancellationToken cancellationToken); + } +} diff --git a/Storage/Source/Internal/Authentication/IAVAuthenticationProvider.cs b/Storage/Source/Internal/Authentication/IAVAuthenticationProvider.cs new file mode 100644 index 0000000..406e37c --- /dev/null +++ b/Storage/Source/Internal/Authentication/IAVAuthenticationProvider.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace LeanCloud.Storage.Internal { + public interface IAVAuthenticationProvider { + /// + /// Authenticates with the service. + /// + /// The cancellation token. + Task> AuthenticateAsync(CancellationToken cancellationToken); + + /// + /// Deauthenticates (logs out) the user associated with this provider. This + /// call may block. + /// + void Deauthenticate(); + + /// + /// Restores authentication that has been serialized, such as session keys, + /// etc. + /// + /// The auth data for the provider. This value may be null + /// when unlinking an account. + /// true iff the authData was successfully synchronized. A false return + /// value indicates that the user should no longer be associated because of bad auth + /// data. + bool RestoreAuthentication(IDictionary authData); + + /// + /// Provides a unique name for the type of authentication the provider does. + /// For example, the FacebookAuthenticationProvider would return "facebook". + /// + string AuthType { get; } + } +} diff --git a/Storage/Source/Internal/Cloud/Controller/AVCloudCodeController.cs b/Storage/Source/Internal/Cloud/Controller/AVCloudCodeController.cs new file mode 100644 index 0000000..c6fa216 --- /dev/null +++ b/Storage/Source/Internal/Cloud/Controller/AVCloudCodeController.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using LeanCloud.Utilities; +using LeanCloud.Storage.Internal; + +namespace LeanCloud.Storage.Internal +{ + public class AVCloudCodeController : IAVCloudCodeController + { + private readonly IAVCommandRunner commandRunner; + + public AVCloudCodeController(IAVCommandRunner commandRunner) + { + this.commandRunner = commandRunner; + } + + public Task CallFunctionAsync(String name, + IDictionary parameters, + string sessionToken, + CancellationToken cancellationToken) + { + var command = new AVCommand(string.Format("functions/{0}", Uri.EscapeUriString(name)), + method: "POST", + sessionToken: sessionToken, + data: NoObjectsEncoder.Instance.Encode(parameters) as IDictionary); + + return commandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).OnSuccess(t => + { + var decoded = AVDecoder.Instance.Decode(t.Result.Item2) as IDictionary; + if (!decoded.ContainsKey("result")) + { + return default(T); + } + return Conversion.To(decoded["result"]); + }); + } + + public Task RPCFunction(string name, IDictionary parameters, string sessionToken, CancellationToken cancellationToken) + { + var command = new AVCommand(string.Format("call/{0}", Uri.EscapeUriString(name)), + method: "POST", + sessionToken: sessionToken, + data: PointerOrLocalIdEncoder.Instance.Encode(parameters) as IDictionary); + + return commandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).OnSuccess(t => + { + var decoded = AVDecoder.Instance.Decode(t.Result.Item2) as IDictionary; + if (!decoded.ContainsKey("result")) + { + return default(T); + } + return Conversion.To(decoded["result"]); + }); + } + } +} diff --git a/Storage/Source/Internal/Cloud/Controller/IAVCloudCodeController.cs b/Storage/Source/Internal/Cloud/Controller/IAVCloudCodeController.cs new file mode 100644 index 0000000..ad6138f --- /dev/null +++ b/Storage/Source/Internal/Cloud/Controller/IAVCloudCodeController.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace LeanCloud.Storage.Internal +{ + public interface IAVCloudCodeController + { + Task CallFunctionAsync(String name, + IDictionary parameters, + string sessionToken, + CancellationToken cancellationToken); + + Task RPCFunction(string name, IDictionary parameters, + string sessionToken, + CancellationToken cancellationToken); + } +} diff --git a/Storage/Source/Internal/Command/AVCommand.cs b/Storage/Source/Internal/Command/AVCommand.cs new file mode 100644 index 0000000..0d038bc --- /dev/null +++ b/Storage/Source/Internal/Command/AVCommand.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using LeanCloud.Storage.Internal; +using System.Linq; + +namespace LeanCloud.Storage.Internal +{ + /// + /// AVCommand is an with pre-populated + /// headers. + /// + public class AVCommand : HttpRequest + { + public IDictionary DataObject { get; private set; } + public override Stream Data + { + get + { + if (base.Data != null) + { + return base.Data; + } + + return base.Data = (DataObject != null + ? new MemoryStream(Encoding.UTF8.GetBytes(Json.Encode(DataObject))) + : null); + } + set { base.Data = value; } + } + + public AVCommand(string relativeUri, + string method, + string sessionToken = null, + IList> headers = null, + IDictionary data = null) : this(relativeUri: relativeUri, + method: method, + sessionToken: sessionToken, + headers: headers, + stream: null, + contentType: data != null ? "application/json" : null) + { + DataObject = data; + } + + public AVCommand(string relativeUri, + string method, + string sessionToken = null, + IList> headers = null, + Stream stream = null, + string contentType = null) + { + var state = AVPlugins.Instance.AppRouterController.Get(); + var urlTemplate = "https://{0}/{1}/{2}"; + AVClient.Configuration configuration = AVClient.CurrentConfiguration; + var apiVersion = "1.1"; + if (relativeUri.StartsWith("push") || relativeUri.StartsWith("installations")) + { + Uri = new Uri(string.Format(urlTemplate, state.PushServer, apiVersion, relativeUri)); + if (configuration.PushServer != null) + { + Uri = new Uri(string.Format("{0}{1}/{2}", configuration.PushServer, apiVersion, relativeUri)); + } + } + else if (relativeUri.StartsWith("stats") || relativeUri.StartsWith("always_collect") || relativeUri.StartsWith("statistics")) + { + Uri = new Uri(string.Format(urlTemplate, state.StatsServer, apiVersion, relativeUri)); + if (configuration.StatsServer != null) + { + Uri = new Uri(string.Format("{0}{1}/{2}", configuration.StatsServer, apiVersion, relativeUri)); + } + } + else if (relativeUri.StartsWith("functions") || relativeUri.StartsWith("call")) + { + Uri = new Uri(string.Format(urlTemplate, state.EngineServer, apiVersion, relativeUri)); + + if (configuration.EngineServer != null) + { + Uri = new Uri(string.Format("{0}{1}/{2}", configuration.EngineServer, apiVersion, relativeUri)); + } + } + else + { + Uri = new Uri(string.Format(urlTemplate, state.ApiServer, apiVersion, relativeUri)); + + if (configuration.ApiServer != null) + { + Uri = new Uri(string.Format("{0}{1}/{2}", configuration.ApiServer, apiVersion, relativeUri)); + } + } + Method = method; + Data = stream; + Headers = new List>(headers ?? Enumerable.Empty>()); + + string useProduction = AVClient.UseProduction ? "1" : "0"; + Headers.Add(new KeyValuePair("X-LC-Prod", useProduction)); + + if (!string.IsNullOrEmpty(sessionToken)) + { + Headers.Add(new KeyValuePair("X-LC-Session", sessionToken)); + } + if (!string.IsNullOrEmpty(contentType)) + { + Headers.Add(new KeyValuePair("Content-Type", contentType)); + } + } + + public AVCommand(AVCommand other) + { + this.Uri = other.Uri; + this.Method = other.Method; + this.DataObject = other.DataObject; + this.Headers = new List>(other.Headers); + this.Data = other.Data; + } + } +} diff --git a/Storage/Source/Internal/Command/AVCommandRunner.cs b/Storage/Source/Internal/Command/AVCommandRunner.cs new file mode 100644 index 0000000..cfbfc18 --- /dev/null +++ b/Storage/Source/Internal/Command/AVCommandRunner.cs @@ -0,0 +1,167 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using LeanCloud.Storage.Internal; + +namespace LeanCloud.Storage.Internal +{ + /// + /// Command Runner. + /// + public class AVCommandRunner : IAVCommandRunner + { + private readonly IHttpClient httpClient; + private readonly IInstallationIdController installationIdController; + + /// + /// + /// + /// + /// + public AVCommandRunner(IHttpClient httpClient, IInstallationIdController installationIdController) + { + this.httpClient = httpClient; + this.installationIdController = installationIdController; + } + + /// + /// + /// + /// + /// + /// + /// + /// + public Task>> RunCommandAsync(AVCommand command, + IProgress uploadProgress = null, + IProgress downloadProgress = null, + CancellationToken cancellationToken = default(CancellationToken)) + { + return PrepareCommand(command).ContinueWith(commandTask => + { + var requestLog = commandTask.Result.ToLog(); + AVClient.PrintLog("http=>" + requestLog); + + return httpClient.ExecuteAsync(commandTask.Result, uploadProgress, downloadProgress, cancellationToken).OnSuccess(t => + { + cancellationToken.ThrowIfCancellationRequested(); + + var response = t.Result; + var contentString = response.Item2; + int responseCode = (int)response.Item1; + + var responseLog = responseCode + ";" + contentString; + AVClient.PrintLog("http<=" + responseLog); + + if (responseCode >= 500) + { + // Server error, return InternalServerError. + throw new AVException(AVException.ErrorCode.InternalServerError, response.Item2); + } + else if (contentString != null) + { + IDictionary contentJson = null; + try + { + if (contentString.StartsWith("[")) + { + var arrayJson = Json.Parse(contentString); + contentJson = new Dictionary { { "results", arrayJson } }; + } + else + { + contentJson = Json.Parse(contentString) as IDictionary; + } + } + catch (Exception e) + { + throw new AVException(AVException.ErrorCode.OtherCause, + "Invalid response from server", e); + } + if (responseCode < 200 || responseCode > 299) + { + AVClient.PrintLog("error response code:" + responseCode); + int code = (int)(contentJson.ContainsKey("code") ? (int)contentJson["code"] : (int)AVException.ErrorCode.OtherCause); + string error = contentJson.ContainsKey("error") ? + contentJson["error"] as string : contentString; + AVException.ErrorCode ec = (AVException.ErrorCode)code; + throw new AVException(ec, error); + } + return new Tuple>(response.Item1, + contentJson); + } + return new Tuple>(response.Item1, null); + }); + }).Unwrap(); + } + + private const string revocableSessionTokenTrueValue = "1"; + private Task PrepareCommand(AVCommand command) + { + AVCommand newCommand = new AVCommand(command); + + Task installationIdTask = installationIdController.GetAsync().ContinueWith(t => + { + newCommand.Headers.Add(new KeyValuePair("X-LC-Installation-Id", t.Result.ToString())); + return newCommand; + }); + + AVClient.Configuration configuration = AVClient.CurrentConfiguration; + newCommand.Headers.Add(new KeyValuePair("X-LC-Id", configuration.ApplicationId)); + + long timestamp = (long)(DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalMilliseconds; + if (!string.IsNullOrEmpty(configuration.MasterKey) && AVClient.UseMasterKey) + { + string sign = MD5.GetMd5String(timestamp + configuration.MasterKey); + newCommand.Headers.Add(new KeyValuePair("X-LC-Sign", string.Format("{0},{1},master", sign, timestamp))); + } + else + { + string sign = MD5.GetMd5String(timestamp + configuration.ApplicationKey); + newCommand.Headers.Add(new KeyValuePair("X-LC-Sign", string.Format("{0},{1}", sign, timestamp))); + } + + newCommand.Headers.Add(new KeyValuePair("X-LC-Client-Version", AVClient.VersionString)); + + if (!string.IsNullOrEmpty(configuration.VersionInfo.BuildVersion)) + { + newCommand.Headers.Add(new KeyValuePair("X-LC-App-Build-Version", configuration.VersionInfo.BuildVersion)); + } + if (!string.IsNullOrEmpty(configuration.VersionInfo.DisplayVersion)) + { + newCommand.Headers.Add(new KeyValuePair("X-LC-App-Display-Version", configuration.VersionInfo.DisplayVersion)); + } + if (!string.IsNullOrEmpty(configuration.VersionInfo.OSVersion)) + { + newCommand.Headers.Add(new KeyValuePair("X-LC-OS-Version", configuration.VersionInfo.OSVersion)); + } + + if (AVUser.IsRevocableSessionEnabled) + { + newCommand.Headers.Add(new KeyValuePair("X-LeanCloud-Revocable-Session", revocableSessionTokenTrueValue)); + } + + if (configuration.AdditionalHTTPHeaders != null) + { + var headersDictionary = newCommand.Headers.ToDictionary(kv => kv.Key, kv => kv.Value); + foreach (var header in configuration.AdditionalHTTPHeaders) + { + if (headersDictionary.ContainsKey(header.Key)) + { + headersDictionary[header.Key] = header.Value; + } + else + { + newCommand.Headers.Add(header); + } + } + newCommand.Headers = headersDictionary.ToList(); + } + + return installationIdTask; + } + } +} diff --git a/Storage/Source/Internal/Command/IAVCommandRunner.cs b/Storage/Source/Internal/Command/IAVCommandRunner.cs new file mode 100644 index 0000000..081623b --- /dev/null +++ b/Storage/Source/Internal/Command/IAVCommandRunner.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Threading; +using System.Threading.Tasks; + +namespace LeanCloud.Storage.Internal +{ + public interface IAVCommandRunner + { + /// + /// Executes and convert the result into Dictionary. + /// + /// The command to be run. + /// Upload progress callback. + /// Download progress callback. + /// The cancellation token for the request. + /// + Task>> RunCommandAsync(AVCommand command, + IProgress uploadProgress = null, + IProgress downloadProgress = null, + CancellationToken cancellationToken = default(CancellationToken)); + } +} diff --git a/Storage/Source/Internal/Config/Controller/AVConfigController.cs b/Storage/Source/Internal/Config/Controller/AVConfigController.cs new file mode 100644 index 0000000..15d34ff --- /dev/null +++ b/Storage/Source/Internal/Config/Controller/AVConfigController.cs @@ -0,0 +1,40 @@ +using System; +using System.Threading.Tasks; +using System.Threading; +using LeanCloud.Storage.Internal; + +namespace LeanCloud.Storage.Internal { + /// + /// Config controller. + /// + internal class AVConfigController : IAVConfigController { + private readonly IAVCommandRunner commandRunner; + + /// + /// Initializes a new instance of the class. + /// + public AVConfigController(IAVCommandRunner commandRunner, IStorageController storageController) { + this.commandRunner = commandRunner; + CurrentConfigController = new AVCurrentConfigController(storageController); + } + + public IAVCommandRunner CommandRunner { get; internal set; } + public IAVCurrentConfigController CurrentConfigController { get; internal set; } + + public Task FetchConfigAsync(String sessionToken, CancellationToken cancellationToken) { + var command = new AVCommand("config", + method: "GET", + sessionToken: sessionToken, + data: null); + + return commandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).OnSuccess(task => { + cancellationToken.ThrowIfCancellationRequested(); + return new AVConfig(task.Result.Item2); + }).OnSuccess(task => { + cancellationToken.ThrowIfCancellationRequested(); + CurrentConfigController.SetCurrentConfigAsync(task.Result); + return task; + }).Unwrap(); + } + } +} diff --git a/Storage/Source/Internal/Config/Controller/AVCurrentConfigController.cs b/Storage/Source/Internal/Config/Controller/AVCurrentConfigController.cs new file mode 100644 index 0000000..41ba8d8 --- /dev/null +++ b/Storage/Source/Internal/Config/Controller/AVCurrentConfigController.cs @@ -0,0 +1,76 @@ +using System; +using System.Threading.Tasks; +using System.Threading; +using System.Collections.Generic; +using LeanCloud.Storage.Internal; + +namespace LeanCloud.Storage.Internal { + /// + /// LeanCloud current config controller. + /// + internal class AVCurrentConfigController : IAVCurrentConfigController { + private const string CurrentConfigKey = "CurrentConfig"; + + private readonly TaskQueue taskQueue; + private AVConfig currentConfig; + + private IStorageController storageController; + + /// + /// Initializes a new instance of the class. + /// + public AVCurrentConfigController(IStorageController storageController) { + this.storageController = storageController; + + taskQueue = new TaskQueue(); + } + + public Task GetCurrentConfigAsync() { + return taskQueue.Enqueue(toAwait => toAwait.ContinueWith(_ => { + if (currentConfig == null) { + return storageController.LoadAsync().OnSuccess(t => { + object tmp; + t.Result.TryGetValue(CurrentConfigKey, out tmp); + + string propertiesString = tmp as string; + if (propertiesString != null) { + var dictionary = AVClient.DeserializeJsonString(propertiesString); + currentConfig = new AVConfig(dictionary); + } else { + currentConfig = new AVConfig(); + } + + return currentConfig; + }); + } + + return Task.FromResult(currentConfig); + }), CancellationToken.None).Unwrap(); + } + + public Task SetCurrentConfigAsync(AVConfig config) { + return taskQueue.Enqueue(toAwait => toAwait.ContinueWith(_ => { + currentConfig = config; + + var jsonObject = ((IJsonConvertible)config).ToJSON(); + var jsonString = AVClient.SerializeJsonString(jsonObject); + + return storageController.LoadAsync().OnSuccess(t => t.Result.AddAsync(CurrentConfigKey, jsonString)); + }).Unwrap().Unwrap(), CancellationToken.None); + } + + public Task ClearCurrentConfigAsync() { + return taskQueue.Enqueue(toAwait => toAwait.ContinueWith(_ => { + currentConfig = null; + + return storageController.LoadAsync().OnSuccess(t => t.Result.RemoveAsync(CurrentConfigKey)); + }).Unwrap().Unwrap(), CancellationToken.None); + } + + public Task ClearCurrentConfigInMemoryAsync() { + return taskQueue.Enqueue(toAwait => toAwait.ContinueWith(_ => { + currentConfig = null; + }), CancellationToken.None); + } + } +} diff --git a/Storage/Source/Internal/Config/Controller/IAVConfigController.cs b/Storage/Source/Internal/Config/Controller/IAVConfigController.cs new file mode 100644 index 0000000..5cef0ac --- /dev/null +++ b/Storage/Source/Internal/Config/Controller/IAVConfigController.cs @@ -0,0 +1,21 @@ +using System; +using System.Threading.Tasks; +using System.Threading; + +namespace LeanCloud.Storage.Internal { + public interface IAVConfigController { + /// + /// Gets the current config controller. + /// + /// The current config controller. + IAVCurrentConfigController CurrentConfigController { get; } + + /// + /// Fetches the config from the server asynchronously. + /// + /// The config async. + /// Session token. + /// Cancellation token. + Task FetchConfigAsync(String sessionToken, CancellationToken cancellationToken); + } +} diff --git a/Storage/Source/Internal/Config/Controller/IAVCurrentConfigController.cs b/Storage/Source/Internal/Config/Controller/IAVCurrentConfigController.cs new file mode 100644 index 0000000..759329c --- /dev/null +++ b/Storage/Source/Internal/Config/Controller/IAVCurrentConfigController.cs @@ -0,0 +1,31 @@ +using System; +using System.Threading.Tasks; + +namespace LeanCloud.Storage.Internal { + public interface IAVCurrentConfigController { + /// + /// Gets the current config async. + /// + /// The current config async. + Task GetCurrentConfigAsync(); + + /// + /// Sets the current config async. + /// + /// The current config async. + /// Config. + Task SetCurrentConfigAsync(AVConfig config); + + /// + /// Clears the current config async. + /// + /// The current config async. + Task ClearCurrentConfigAsync(); + + /// + /// Clears the current config in memory async. + /// + /// The current config in memory async. + Task ClearCurrentConfigInMemoryAsync(); + } +} diff --git a/Storage/Source/Internal/Dispatcher/Unity/UnityDispatcher.cs b/Storage/Source/Internal/Dispatcher/Unity/UnityDispatcher.cs new file mode 100644 index 0000000..3a061e2 --- /dev/null +++ b/Storage/Source/Internal/Dispatcher/Unity/UnityDispatcher.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Threading; +using UnityEngine; + +namespace LeanCloud.Storage.Internal +{ + /// + /// This class represents the internal Unity dispatcher used by the LeanCloud SDK. + /// + /// It should be initialized once in your game, usually via AVInitializeBehavior. + /// + /// In certain, advanced use-cases, you may wish to use + /// this to set up your dispatcher manually. + /// + // TODO: (richardross) Review this interface before going public. + public sealed class Dispatcher + { + static Dispatcher() + { + Instance = new Dispatcher(); + } + + private Dispatcher() + { + DispatcherCoroutine = CreateDispatcherCoroutine(); + } + + public static Dispatcher Instance { get; private set; } + + public GameObject GameObject { get; set; } + public IEnumerator DispatcherCoroutine { get; private set; } + + private readonly ReaderWriterLockSlim dispatchQueueLock = new ReaderWriterLockSlim(); + private readonly Queue dispatchQueue = new Queue(); + + public void Post(Action action) + { + if (dispatchQueueLock.IsWriteLockHeld) + { + dispatchQueue.Enqueue(action); + return; + } + + dispatchQueueLock.EnterWriteLock(); + try + { + dispatchQueue.Enqueue(action); + } + finally + { + dispatchQueueLock.ExitWriteLock(); + } + } + + private IEnumerator CreateDispatcherCoroutine() + { + // We must stop the first invocation here, so that we don't actually do anything until we begin looping. + yield return null; + while (true) + { + dispatchQueueLock.EnterUpgradeableReadLock(); + try + { + // We'll only empty what's already in the dispatch queue in this iteration (so that a + // nested dispatch behaves like nextTick()). + int count = dispatchQueue.Count; + if (count > 0) + { + dispatchQueueLock.EnterWriteLock(); + try + { + while (count > 0) + { + try + { + dispatchQueue.Dequeue()(); + } + catch (Exception e) + { + // If an exception occurs, catch it and log it so that dispatches aren't broken. + Debug.LogException(e); + } + count--; + } + } + finally + { + dispatchQueueLock.ExitWriteLock(); + } + } + } + finally + { + dispatchQueueLock.ExitUpgradeableReadLock(); + } + yield return null; + } + } + } +} diff --git a/Storage/Source/Internal/Encoding/AVDecoder.cs b/Storage/Source/Internal/Encoding/AVDecoder.cs new file mode 100644 index 0000000..fdb4303 --- /dev/null +++ b/Storage/Source/Internal/Encoding/AVDecoder.cs @@ -0,0 +1,164 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using System.Globalization; +using LeanCloud.Utilities; + +namespace LeanCloud.Storage.Internal +{ + public class AVDecoder + { + // This class isn't really a Singleton, but since it has no state, it's more efficient to get + // the default instance. + private static readonly AVDecoder instance = new AVDecoder(); + public static AVDecoder Instance + { + get + { + return instance; + } + } + + // Prevent default constructor. + private AVDecoder() { } + + public object Decode(object data) + { + if (data == null) + { + return null; + } + + var dict = data as IDictionary; + if (dict != null) + { + if (dict.ContainsKey("__op")) + { + return AVFieldOperations.Decode(dict); + } + + object type; + dict.TryGetValue("__type", out type); + var typeString = type as string; + + if (typeString == null) + { + var newDict = new Dictionary(); + foreach (var pair in dict) + { + newDict[pair.Key] = Decode(pair.Value); + } + return newDict; + } + + if (typeString == "Date") + { + return ParseDate(dict["iso"] as string); + } + + if (typeString == "Bytes") + { + return Convert.FromBase64String(dict["base64"] as string); + } + + if (typeString == "Pointer") + { + //set a include key to fetch or query. + if (dict.Keys.Count > 3) + { + return DecodeAVObject(dict); + } + return DecodePointer(dict["className"] as string, dict["objectId"] as string); + } + + if (typeString == "File") + { + return DecodeAVFile(dict); + } + + if (typeString == "GeoPoint") + { + return new AVGeoPoint(Conversion.To(dict["latitude"]), + Conversion.To(dict["longitude"])); + } + + if (typeString == "Object") + { + return DecodeAVObject(dict); + } + + if (typeString == "Relation") + { + return AVRelationBase.CreateRelation(null, null, dict["className"] as string); + } + + var converted = new Dictionary(); + foreach (var pair in dict) + { + converted[pair.Key] = Decode(pair.Value); + } + return converted; + } + + var list = data as IList; + if (list != null) + { + return (from item in list + select Decode(item)).ToList(); + } + + return data; + } + + protected virtual object DecodePointer(string className, string objectId) + { + if (className == "_File") + { + return AVFile.CreateWithoutData(objectId); + } + return AVObject.CreateWithoutData(className, objectId); + } + protected virtual object DecodeAVObject(IDictionary dict) + { + var className = dict["className"] as string; + if (className == "_File") + { + return DecodeAVFile(dict); + } + var state = AVObjectCoder.Instance.Decode(dict, this); + return AVObject.FromState(state, dict["className"] as string); + } + protected virtual object DecodeAVFile(IDictionary dict) + { + var objectId = dict["objectId"] as string; + var file = AVFile.CreateWithoutData(objectId); + file.MergeFromJSON(dict); + return file; + } + + + public virtual IList DecodeList(object data) + { + IList rtn = null; + var list = (IList)data; + if (list != null) + { + rtn = new List(); + foreach (var item in list) + { + rtn.Add((T)item); + } + } + return rtn; + } + + public static DateTime ParseDate(string input) + { + var rtn = DateTime.ParseExact(input, + AVClient.DateFormatStrings, + CultureInfo.InvariantCulture, + DateTimeStyles.AssumeUniversal); + return rtn; + } + } +} diff --git a/Storage/Source/Internal/Encoding/AVEncoder.cs b/Storage/Source/Internal/Encoding/AVEncoder.cs new file mode 100644 index 0000000..401edc1 --- /dev/null +++ b/Storage/Source/Internal/Encoding/AVEncoder.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using LeanCloud.Utilities; +using LeanCloud.Storage.Internal; + +namespace LeanCloud.Storage.Internal +{ + /// + /// A AVEncoder can be used to transform objects such as into JSON + /// data structures. + /// + /// + public abstract class AVEncoder + { +#if UNITY + private static readonly bool isCompiledByIL2CPP = AppDomain.CurrentDomain.FriendlyName.Equals("IL2CPP Root Domain"); +#else + private static readonly bool isCompiledByIL2CPP = false; +#endif + + public static bool IsValidType(object value) + { + return value == null || + ReflectionHelpers.IsPrimitive(value.GetType()) || + value is string || + value is AVObject || + value is AVACL || + value is AVFile || + value is AVGeoPoint || + value is AVRelationBase || + value is DateTime || + value is byte[] || + Conversion.As>(value) != null || + Conversion.As>(value) != null; + } + + public object Encode(object value) + { + // If this object has a special encoding, encode it and return the + // encoded object. Otherwise, just return the original object. + if (value is DateTime) + { + return new Dictionary + { + { + "iso", ((DateTime)value).ToUniversalTime().ToString(AVClient.DateFormatStrings.First(), CultureInfo.InvariantCulture) + }, + { + "__type", "Date" + } + }; + } + + if (value is AVFile) + { + var file = value as AVFile; + return new Dictionary + { + {"__type", "Pointer"}, + { "className", "_File"}, + { "objectId", file.ObjectId} + }; + } + + var bytes = value as byte[]; + if (bytes != null) + { + return new Dictionary + { + { "__type", "Bytes"}, + { "base64", Convert.ToBase64String(bytes)} + }; + } + + var obj = value as AVObject; + if (obj != null) + { + return EncodeAVObject(obj); + } + + var jsonConvertible = value as IJsonConvertible; + if (jsonConvertible != null) + { + return jsonConvertible.ToJSON(); + } + + var dict = Conversion.As>(value); + if (dict != null) + { + var json = new Dictionary(); + foreach (var pair in dict) + { + json[pair.Key] = Encode(pair.Value); + } + return json; + } + + var list = Conversion.As>(value); + if (list != null) + { + return EncodeList(list); + } + + // TODO (hallucinogen): convert IAVFieldOperation to IJsonConvertible + var operation = value as IAVFieldOperation; + if (operation != null) + { + return operation.Encode(); + } + + return value; + } + + protected abstract IDictionary EncodeAVObject(AVObject value); + + private object EncodeList(IList list) + { + var newArray = new List(); + // We need to explicitly cast `list` to `List` rather than + // `IList` because IL2CPP is stricter than the usual Unity AOT compiler pipeline. + if (isCompiledByIL2CPP && list.GetType().IsArray) + { + list = new List(list); + } + foreach (var item in list) + { + if (!IsValidType(item)) + { + throw new ArgumentException("Invalid type for value in an array"); + } + newArray.Add(Encode(item)); + } + return newArray; + } + } +} diff --git a/Storage/Source/Internal/Encoding/AVObjectCoder.cs b/Storage/Source/Internal/Encoding/AVObjectCoder.cs new file mode 100644 index 0000000..05a6509 --- /dev/null +++ b/Storage/Source/Internal/Encoding/AVObjectCoder.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections.Generic; + +namespace LeanCloud.Storage.Internal +{ + // TODO: (richardross) refactor entire LeanCloud coder interfaces. + public class AVObjectCoder + { + private static readonly AVObjectCoder instance = new AVObjectCoder(); + public static AVObjectCoder Instance + { + get + { + return instance; + } + } + + // Prevent default constructor. + private AVObjectCoder() { } + + public IDictionary Encode(T state, + IDictionary operations, + AVEncoder encoder) where T : IObjectState + { + var result = new Dictionary(); + foreach (var pair in operations) + { + // AVRPCSerialize the data + var operation = pair.Value; + + result[pair.Key] = encoder.Encode(operation); + } + + return result; + } + + public IObjectState Decode(IDictionary data, + AVDecoder decoder) + { + IDictionary serverData = new Dictionary(); + var mutableData = new Dictionary(data); + string objectId = extractFromDictionary(mutableData, "objectId", (obj) => + { + return obj as string; + }); + DateTime? createdAt = extractFromDictionary(mutableData, "createdAt", (obj) => + { + return AVDecoder.ParseDate(obj as string); + }); + DateTime? updatedAt = extractFromDictionary(mutableData, "updatedAt", (obj) => + { + return AVDecoder.ParseDate(obj as string); + }); + + if (mutableData.ContainsKey("ACL")) + { + serverData["ACL"] = extractFromDictionary(mutableData, "ACL", (obj) => + { + return new AVACL(obj as IDictionary); + }); + } + string className = extractFromDictionary(mutableData, "className", obj => + { + return obj as string; + }); + if (createdAt != null && updatedAt == null) + { + updatedAt = createdAt; + } + + // Bring in the new server data. + foreach (var pair in mutableData) + { + if (pair.Key == "__type" || pair.Key == "className") + { + continue; + } + + var value = pair.Value; + serverData[pair.Key] = decoder.Decode(value); + } + + return new MutableObjectState + { + ObjectId = objectId, + CreatedAt = createdAt, + UpdatedAt = updatedAt, + ServerData = serverData, + ClassName = className + }; + } + + private T extractFromDictionary(IDictionary data, string key, Func action) + { + T result = default(T); + if (data.ContainsKey(key)) + { + result = action(data[key]); + data.Remove(key); + } + + return result; + } + } +} diff --git a/Storage/Source/Internal/Encoding/NoObjectsEncoder.cs b/Storage/Source/Internal/Encoding/NoObjectsEncoder.cs new file mode 100644 index 0000000..6a1e7e1 --- /dev/null +++ b/Storage/Source/Internal/Encoding/NoObjectsEncoder.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; + +namespace LeanCloud.Storage.Internal { + /// + /// A that throws an exception if it attempts to encode + /// a + /// + public class NoObjectsEncoder : AVEncoder { + // This class isn't really a Singleton, but since it has no state, it's more efficient to get + // the default instance. + private static readonly NoObjectsEncoder instance = new NoObjectsEncoder(); + public static NoObjectsEncoder Instance { + get { + return instance; + } + } + + protected override IDictionary EncodeAVObject(AVObject value) { + throw new ArgumentException("AVObjects not allowed here."); + } + } +} diff --git a/Storage/Source/Internal/Encoding/PointerOrLocalIdEncoder.cs b/Storage/Source/Internal/Encoding/PointerOrLocalIdEncoder.cs new file mode 100644 index 0000000..2394cf7 --- /dev/null +++ b/Storage/Source/Internal/Encoding/PointerOrLocalIdEncoder.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +namespace LeanCloud.Storage.Internal +{ + /// + /// A that encode as pointers. If the object + /// does not have an , uses a local id. + /// + public class PointerOrLocalIdEncoder : AVEncoder + { + // This class isn't really a Singleton, but since it has no state, it's more efficient to get + // the default instance. + private static readonly PointerOrLocalIdEncoder instance = new PointerOrLocalIdEncoder(); + public static PointerOrLocalIdEncoder Instance + { + get + { + return instance; + } + } + + protected override IDictionary EncodeAVObject(AVObject value) + { + if (value.ObjectId == null) + { + // TODO (hallucinogen): handle local id. For now we throw. + throw new ArgumentException("Cannot create a pointer to an object without an objectId"); + } + + return new Dictionary { + {"__type", "Pointer"}, + { "className", value.ClassName}, + { "objectId", value.ObjectId} + }; + } + + public IDictionary EncodeAVObject(AVObject value, bool isPointer) + { + if (isPointer) + { + return EncodeAVObject(value); + } + var operations = value.GetCurrentOperations(); + var operationJSON = AVObject.ToJSONObjectForSaving(operations); + var objectJSON = value.ToDictionary(kvp => kvp.Key, kvp => PointerOrLocalIdEncoder.Instance.Encode(kvp.Value)); + foreach (var kvp in operationJSON) + { + objectJSON[kvp.Key] = kvp.Value; + } + if (value.CreatedAt.HasValue) + { + objectJSON["createdAt"] = value.CreatedAt.Value.ToString(AVClient.DateFormatStrings.First(), + CultureInfo.InvariantCulture); + } + if (value.UpdatedAt.HasValue) + { + objectJSON["updatedAt"] = value.UpdatedAt.Value.ToString(AVClient.DateFormatStrings.First(), + CultureInfo.InvariantCulture); + } + if(!string.IsNullOrEmpty(value.ObjectId)) + { + objectJSON["objectId"] = value.ObjectId; + } + objectJSON["className"] = value.ClassName; + objectJSON["__type"] = "Object"; + return objectJSON; + } + } +} diff --git a/Storage/Source/Internal/File/Controller/AVFileController.cs b/Storage/Source/Internal/File/Controller/AVFileController.cs new file mode 100644 index 0000000..688689b --- /dev/null +++ b/Storage/Source/Internal/File/Controller/AVFileController.cs @@ -0,0 +1,151 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using LeanCloud.Storage.Internal; +using System.Net; +using System.Collections.Generic; +using System.Linq; + +namespace LeanCloud.Storage.Internal +{ + /// + /// AVF ile controller. + /// + public class AVFileController : IAVFileController + { + private readonly IAVCommandRunner commandRunner; + /// + /// Initializes a new instance of the class. + /// + /// Command runner. + public AVFileController(IAVCommandRunner commandRunner) + { + this.commandRunner = commandRunner; + } + /// + /// Saves the async. + /// + /// The async. + /// State. + /// Data stream. + /// Session token. + /// Progress. + /// Cancellation token. + public virtual Task SaveAsync(FileState state, + Stream dataStream, + String sessionToken, + IProgress progress, + CancellationToken cancellationToken = default(CancellationToken)) + { + if (state.Url != null) + { + // !isDirty + return Task.FromResult(state); + } + + if (cancellationToken.IsCancellationRequested) + { + var tcs = new TaskCompletionSource(); + tcs.TrySetCanceled(); + return tcs.Task; + } + + var oldPosition = dataStream.Position; + var command = new AVCommand("files/" + state.Name, + method: "POST", + sessionToken: sessionToken, + contentType: state.MimeType, + stream: dataStream); + + return commandRunner.RunCommandAsync(command, + uploadProgress: progress, + cancellationToken: cancellationToken).OnSuccess(uploadTask => + { + var result = uploadTask.Result; + var jsonData = result.Item2; + cancellationToken.ThrowIfCancellationRequested(); + + return new FileState + { + Name = jsonData["name"] as string, + Url = new Uri(jsonData["url"] as string, UriKind.Absolute), + MimeType = state.MimeType + }; + }).ContinueWith(t => + { + // Rewind the stream on failure or cancellation (if possible) + if ((t.IsFaulted || t.IsCanceled) && dataStream.CanSeek) + { + dataStream.Seek(oldPosition, SeekOrigin.Begin); + } + return t; + }).Unwrap(); + } + public Task DeleteAsync(FileState state, string sessionToken, CancellationToken cancellationToken) + { + var command = new AVCommand("files/" + state.ObjectId, + method: "DELETE", + sessionToken: sessionToken, + data: null); + + return commandRunner.RunCommandAsync(command, cancellationToken: cancellationToken); + } + internal static Task>> GetFileToken(FileState fileState, CancellationToken cancellationToken) + { + Task>> rtn; + string currentSessionToken = AVUser.CurrentSessionToken; + string str = fileState.Name; + IDictionary parameters = new Dictionary(); + parameters.Add("name", str); + parameters.Add("key", GetUniqueName(fileState)); + parameters.Add("__type", "File"); + parameters.Add("mime_type", AVFile.GetMIMEType(str)); + parameters.Add("metaData", fileState.MetaData); + + rtn = AVClient.RequestAsync("POST", new Uri("fileTokens", UriKind.Relative), currentSessionToken, parameters, cancellationToken); + + return rtn; + } + public Task GetAsync(string objectId, string sessionToken, CancellationToken cancellationToken) + { + var command = new AVCommand("files/" + objectId, + method: "GET", + sessionToken: sessionToken, + data: null); + + return commandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).OnSuccess(_ => + { + var result = _.Result; + var jsonData = result.Item2; + cancellationToken.ThrowIfCancellationRequested(); + return new FileState + { + ObjectId = jsonData["objectId"] as string, + Name = jsonData["name"] as string, + Url = new Uri(jsonData["url"] as string, UriKind.Absolute), + }; + }); + } + internal static string GetUniqueName(FileState fileState) + { + string key = Random(12); + string extension = Path.GetExtension(fileState.Name); + key += extension; + fileState.CloudName = key; + return key; + } + internal static string Random(int length) + { + const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz"; + var random = new Random(); + return new string(Enumerable.Repeat(chars, length) + .Select(s => s[random.Next(s.Length)]).ToArray()); + } + internal static double CalcProgress(double already, double total) + { + var pv = (1.0 * already / total); + return Math.Round(pv, 3); + } + } +} diff --git a/Storage/Source/Internal/File/Controller/AWSS3FileController.cs b/Storage/Source/Internal/File/Controller/AWSS3FileController.cs new file mode 100644 index 0000000..d812f20 --- /dev/null +++ b/Storage/Source/Internal/File/Controller/AWSS3FileController.cs @@ -0,0 +1,54 @@ +using System; +using System.Threading.Tasks; +using System.Threading; +using System.IO; +using LeanCloud.Storage.Internal; +using System.Collections.Generic; + +namespace LeanCloud.Storage.Internal +{ + internal class AWSS3FileController : AVFileController + { + + private object mutex = new object(); + + + public AWSS3FileController(IAVCommandRunner commandRunner) : base(commandRunner) + { + + } + + public override Task SaveAsync(FileState state, Stream dataStream, string sessionToken, IProgress progress, CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (state.Url != null) + { + return Task.FromResult(state); + } + + return GetFileToken(state, cancellationToken).OnSuccess(t => + { + var fileToken = t.Result.Item2; + var uploadUrl = fileToken["upload_url"].ToString(); + state.ObjectId = fileToken["objectId"].ToString(); + string url = fileToken["url"] as string; + state.Url = new Uri(url, UriKind.Absolute); + return PutFile(state, uploadUrl, dataStream); + + }).Unwrap().OnSuccess(s => + { + return s.Result; + }); + } + + internal Task PutFile(FileState state, string uploadUrl, Stream dataStream) + { + IList> makeBlockHeaders = new List>(); + makeBlockHeaders.Add(new KeyValuePair("Content-Type", state.MimeType)); + + return AVClient.RequestAsync(new Uri(uploadUrl), "PUT", makeBlockHeaders, dataStream, state.MimeType, CancellationToken.None).OnSuccess(t => + { + return state; + }); + } + } +} diff --git a/Storage/Source/Internal/File/Controller/IAVFileController.cs b/Storage/Source/Internal/File/Controller/IAVFileController.cs new file mode 100644 index 0000000..2dea784 --- /dev/null +++ b/Storage/Source/Internal/File/Controller/IAVFileController.cs @@ -0,0 +1,24 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace LeanCloud.Storage.Internal +{ + public interface IAVFileController + { + Task SaveAsync(FileState state, + Stream dataStream, + String sessionToken, + IProgress progress, + CancellationToken cancellationToken); + + Task DeleteAsync(FileState state, + string sessionToken, + CancellationToken cancellationToken); + + Task GetAsync(string objectId, + string sessionToken, + CancellationToken cancellationToken); + } +} diff --git a/Storage/Source/Internal/File/Controller/QCloudCosFileController.cs b/Storage/Source/Internal/File/Controller/QCloudCosFileController.cs new file mode 100644 index 0000000..9eee34a --- /dev/null +++ b/Storage/Source/Internal/File/Controller/QCloudCosFileController.cs @@ -0,0 +1,250 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Linq; +using LeanCloud.Storage.Internal; + +namespace LeanCloud.Storage.Internal +{ + internal class QCloudCosFileController : AVFileController + { + private object mutex = new object(); + + FileState fileState; + Stream data; + string bucket; + string token; + string uploadUrl; + bool done; + private long sliceSize = (long)CommonSize.KB512; + + public QCloudCosFileController(IAVCommandRunner commandRunner) : base(commandRunner) + { + } + + public Task SaveAsync(FileState state, + Stream dataStream, + string sessionToken, + IProgress progress, + CancellationToken cancellationToken) + { + if (state.Url != null) + { + return Task.FromResult(state); + } + fileState = state; + data = dataStream; + return GetFileToken(fileState, cancellationToken).OnSuccess(_ => + { + var fileToken = _.Result.Item2; + uploadUrl = fileToken["upload_url"].ToString(); + token = fileToken["token"].ToString(); + fileState.ObjectId = fileToken["objectId"].ToString(); + bucket = fileToken["bucket"].ToString(); + + return FileSlice(cancellationToken).OnSuccess(t => + { + if (done) return Task.FromResult(state); + var response = t.Result.Item2; + var resumeData = response["data"] as IDictionary; + if (resumeData.ContainsKey("access_url")) return Task.FromResult(state); + var sliceSession = resumeData["session"].ToString(); + var sliceOffset = long.Parse(resumeData["offset"].ToString()); + return UploadSlice(sliceSession, sliceOffset, dataStream, progress, cancellationToken); + }).Unwrap(); + + }).Unwrap(); + } + + Task UploadSlice( + string sessionId, + long offset, + Stream dataStream, + IProgress progress, + CancellationToken cancellationToken) + { + + long dataLength = dataStream.Length; + if (progress != null) + { + lock (mutex) + { + progress.Report(new AVUploadProgressEventArgs() + { + Progress = AVFileController.CalcProgress(offset, dataLength) + }); + } + } + + if (offset == dataLength) + { + return Task.FromResult(fileState); + } + + var sliceFile = GetNextBinary(offset, dataStream); + return ExcuteUpload(sessionId, offset, sliceFile, cancellationToken).OnSuccess(_ => + { + offset += sliceFile.Length; + if (offset == dataLength) + { + done = true; + return Task.FromResult(fileState); + } + var response = _.Result.Item2; + var resumeData = response["data"] as IDictionary; + var sliceSession = resumeData["session"].ToString(); + return UploadSlice(sliceSession, offset, dataStream, progress, cancellationToken); + }).Unwrap(); + } + + Task>> ExcuteUpload(string sessionId, long offset, byte[] sliceFile, CancellationToken cancellationToken) + { + var body = new Dictionary(); + body.Add("op", "upload_slice"); + body.Add("session", sessionId); + body.Add("offset", offset.ToString()); + + return PostToQCloud(body, sliceFile, cancellationToken); + } + + Task>> FileSlice(CancellationToken cancellationToken) + { + SHA1CryptoServiceProvider sha1 = new SHA1CryptoServiceProvider(); + var body = new Dictionary(); + if (data.Length <= (long)CommonSize.KB512) + { + body.Add("op", "upload"); + body.Add("sha", HexStringFromBytes(sha1.ComputeHash(data))); + var wholeFile = GetNextBinary(0, data); + return PostToQCloud(body, wholeFile, cancellationToken).OnSuccess(_ => + { + if (_.Result.Item1 == HttpStatusCode.OK) + { + done = true; + } + return _.Result; + }); + } + else + { + body.Add("op", "upload_slice"); + body.Add("filesize", data.Length); + body.Add("sha", HexStringFromBytes(sha1.ComputeHash(data))); + body.Add("slice_size", (long)CommonSize.KB512); + } + + return PostToQCloud(body, null, cancellationToken); + } + public static string HexStringFromBytes(byte[] bytes) + { + var sb = new StringBuilder(); + foreach (byte b in bytes) + { + var hex = b.ToString("x2"); + sb.Append(hex); + } + return sb.ToString(); + } + + public static string SHA1HashStringForUTF8String(string s) + { + byte[] bytes = Encoding.UTF8.GetBytes(s); + + SHA1CryptoServiceProvider sha1 = new SHA1CryptoServiceProvider(); + byte[] hashBytes = sha1.ComputeHash(bytes); + + return HexStringFromBytes(hashBytes); + } + Task>> PostToQCloud( + Dictionary body, + byte[] sliceFile, + CancellationToken cancellationToken) + { + IList> sliceHeaders = new List>(); + sliceHeaders.Add(new KeyValuePair("Authorization", this.token)); + + string contentType; + long contentLength; + + var tempStream = HttpUploadFile(sliceFile, fileState.CloudName, out contentType, out contentLength, body); + + sliceHeaders.Add(new KeyValuePair("Content-Type", contentType)); + + var rtn = AVClient.RequestAsync(new Uri(this.uploadUrl), "POST", sliceHeaders, tempStream, null, cancellationToken).OnSuccess(_ => + { + var dic = AVClient.ReponseResolve(_.Result, CancellationToken.None); + + return dic; + }); + + return rtn; + } + public static Stream HttpUploadFile(byte[] file, string fileName, out string contentType, out long contentLength, IDictionary nvc) + { + string boundary = "---------------------------" + DateTime.Now.Ticks.ToString("x"); + byte[] boundarybytes = StringToAscii("\r\n--" + boundary + "\r\n"); + contentType = "multipart/form-data; boundary=" + boundary; + + MemoryStream rs = new MemoryStream(); + + string formdataTemplate = "Content-Disposition: form-data; name=\"{0}\"\r\n\r\n{1}"; + foreach (string key in nvc.Keys) + { + rs.Write(boundarybytes, 0, boundarybytes.Length); + string formitem = string.Format(formdataTemplate, key, nvc[key]); + byte[] formitembytes = System.Text.Encoding.UTF8.GetBytes(formitem); + rs.Write(formitembytes, 0, formitembytes.Length); + } + rs.Write(boundarybytes, 0, boundarybytes.Length); + + if (file != null) + { + string headerTemplate = "Content-Disposition: form-data; name=\"{0}\"; filename=\"{1}\"\r\nContent-Type: {2}\r\n\r\n"; + string header = string.Format(headerTemplate, "fileContent", fileName, "application/octet-stream"); + byte[] headerbytes = System.Text.Encoding.UTF8.GetBytes(header); + rs.Write(headerbytes, 0, headerbytes.Length); + + rs.Write(file, 0, file.Length); + } + + byte[] trailer = StringToAscii("\r\n--" + boundary + "--\r\n"); + rs.Write(trailer, 0, trailer.Length); + contentLength = rs.Length; + + rs.Position = 0; + var tempBuffer = new byte[rs.Length]; + rs.Read(tempBuffer, 0, tempBuffer.Length); + + return new MemoryStream(tempBuffer); + } + + public static byte[] StringToAscii(string s) + { + byte[] retval = new byte[s.Length]; + for (int ix = 0; ix < s.Length; ++ix) + { + char ch = s[ix]; + if (ch <= 0x7f) retval[ix] = (byte)ch; + else retval[ix] = (byte)'?'; + } + return retval; + } + + byte[] GetNextBinary(long completed, Stream dataStream) + { + if (completed + sliceSize > dataStream.Length) + { + sliceSize = dataStream.Length - completed; + } + + byte[] chunkBinary = new byte[sliceSize]; + dataStream.Seek(completed, SeekOrigin.Begin); + dataStream.Read(chunkBinary, 0, (int)sliceSize); + return chunkBinary; + } + } +} diff --git a/Storage/Source/Internal/File/Controller/QiniuFileController.cs b/Storage/Source/Internal/File/Controller/QiniuFileController.cs new file mode 100644 index 0000000..72a8c5b --- /dev/null +++ b/Storage/Source/Internal/File/Controller/QiniuFileController.cs @@ -0,0 +1,332 @@ +using LeanCloud.Storage.Internal; +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace LeanCloud.Storage.Internal +{ + internal enum CommonSize : long + { + MB4 = 1024 * 1024 * 4, + MB1 = 1024 * 1024, + KB512 = 1024 * 1024 / 2, + KB256 = 1024 * 1024 / 4 + } + + internal class QiniuFileController : AVFileController + { + private static int BLOCKSIZE = 1024 * 1024 * 4; + private const int blockMashk = (1 << blockBits) - 1; + private const int blockBits = 22; + private int CalcBlockCount(long fsize) + { + return (int)((fsize + blockMashk) >> blockBits); + } + internal static string UP_HOST = "https://up.qbox.me"; + private object mutex = new object(); + + public QiniuFileController(IAVCommandRunner commandRunner) : base(commandRunner) + { + + } + + public override Task SaveAsync(FileState state, + Stream dataStream, + String sessionToken, + IProgress progress, + CancellationToken cancellationToken) + { + if (state.Url != null) + { + return Task.FromResult(state); + } + state.frozenData = dataStream; + state.CloudName = GetUniqueName(state); + return GetQiniuToken(state, CancellationToken.None).ContinueWith(t => + { + MergeFromJSON(state, t.Result.Item2); + return UploadNextChunk(state, dataStream, string.Empty, 0, progress); + }).Unwrap().OnSuccess(s => + { + return state; + }); + } + Task UploadNextChunk(FileState state, Stream dataStream, string context, long offset, IProgress progress) + { + var totalSize = dataStream.Length; + var remainingSize = totalSize - state.completed; + + if (progress != null) + { + lock (mutex) + { + progress.Report(new AVUploadProgressEventArgs() + { + Progress = AVFileController.CalcProgress(state.completed, totalSize) + }); + } + } + if (state.completed == totalSize) + { + return QiniuMakeFile(state, state.frozenData, state.token, state.CloudName, totalSize, state.block_ctxes.ToArray(), CancellationToken.None); + + } + else if (state.completed % BLOCKSIZE == 0) + { + var firstChunkBinary = GetChunkBinary(state.completed, dataStream); + + var blockSize = remainingSize > BLOCKSIZE ? BLOCKSIZE : remainingSize; + return MakeBlock(state, firstChunkBinary, blockSize).ContinueWith(t => + { + + var dic = AVClient.ReponseResolve(t.Result, CancellationToken.None); + var ctx = dic.Item2["ctx"].ToString(); + + offset = long.Parse(dic.Item2["offset"].ToString()); + var host = dic.Item2["host"].ToString(); + + state.completed += firstChunkBinary.Length; + if (state.completed % BLOCKSIZE == 0 || state.completed == totalSize) + { + state.block_ctxes.Add(ctx); + } + + return UploadNextChunk(state, dataStream, ctx, offset, progress); + }).Unwrap(); + + } + else + { + var chunkBinary = GetChunkBinary(state.completed, dataStream); + return PutChunk(state, chunkBinary, context, offset).ContinueWith(t => + { + var dic = AVClient.ReponseResolve(t.Result, CancellationToken.None); + var ctx = dic.Item2["ctx"].ToString(); + + offset = long.Parse(dic.Item2["offset"].ToString()); + var host = dic.Item2["host"].ToString(); + state.completed += chunkBinary.Length; + if (state.completed % BLOCKSIZE == 0 || state.completed == totalSize) + { + state.block_ctxes.Add(ctx); + } + //if (AVClient.fileUploaderDebugLog) + //{ + // AVClient.LogTracker(state.counter + "|completed=" + state.completed + "stream:position=" + dataStream.Position + "|"); + //} + + return UploadNextChunk(state, dataStream, ctx, offset, progress); + }).Unwrap(); + } + } + + byte[] GetChunkBinary(long completed, Stream dataStream) + { + long chunkSize = (long)CommonSize.MB1; + if (completed + chunkSize > dataStream.Length) + { + chunkSize = dataStream.Length - completed; + } + byte[] chunkBinary = new byte[chunkSize]; + dataStream.Seek(completed, SeekOrigin.Begin); + dataStream.Read(chunkBinary, 0, (int)chunkSize); + return chunkBinary; + } + + internal string GetUniqueName(FileState state) + { + string key = Guid.NewGuid().ToString();//file Key in Qiniu. + string extension = Path.GetExtension(state.Name); + key += extension; + return key; + } + internal Task>> GetQiniuToken(FileState state, CancellationToken cancellationToken) + { + Task>> rtn; + string currentSessionToken = AVUser.CurrentSessionToken; + string str = state.Name; + + IDictionary parameters = new Dictionary(); + parameters.Add("name", str); + parameters.Add("key", state.CloudName); + parameters.Add("__type", "File"); + parameters.Add("mime_type", AVFile.GetMIMEType(str)); + + state.MetaData = GetMetaData(state, state.frozenData); + + parameters.Add("metaData", state.MetaData); + + rtn = AVClient.RequestAsync("POST", new Uri("qiniu", UriKind.Relative), currentSessionToken, parameters, cancellationToken); + + return rtn; + } + IList> GetQiniuRequestHeaders(FileState state) + { + IList> makeBlockHeaders = new List>(); + + string authHead = "UpToken " + state.token; + makeBlockHeaders.Add(new KeyValuePair("Authorization", authHead)); + return makeBlockHeaders; + } + + Task> MakeBlock(FileState state, byte[] firstChunkBinary, long blcokSize = 4194304) + { + MemoryStream firstChunkData = new MemoryStream(firstChunkBinary, 0, firstChunkBinary.Length); + return AVClient.RequestAsync(new Uri(new Uri(UP_HOST) + string.Format("mkblk/{0}", blcokSize)), "POST", GetQiniuRequestHeaders(state), firstChunkData, "application/octet-stream", CancellationToken.None); + } + Task> PutChunk(FileState state, byte[] chunkBinary, string LastChunkctx, long currentChunkOffsetInBlock) + { + MemoryStream chunkData = new MemoryStream(chunkBinary, 0, chunkBinary.Length); + return AVClient.RequestAsync(new Uri(new Uri(UP_HOST) + string.Format("bput/{0}/{1}", LastChunkctx, + currentChunkOffsetInBlock)), "POST", + GetQiniuRequestHeaders(state), chunkData, + "application/octet-stream", CancellationToken.None); + } + internal Task> QiniuMakeFile(FileState state, Stream dataStream, string upToken, string key, long fsize, string[] ctxes, CancellationToken cancellationToken) + { + StringBuilder urlBuilder = new StringBuilder(); + urlBuilder.AppendFormat("{0}/mkfile/{1}", UP_HOST, fsize); + if (key != null) + { + urlBuilder.AppendFormat("/key/{0}", ToBase64URLSafe(key)); + } + var metaData = GetMetaData(state, dataStream); + + StringBuilder sb = new StringBuilder(); + foreach (string _key in metaData.Keys) + { + sb.AppendFormat("/{0}/{1}", _key, ToBase64URLSafe(metaData[_key].ToString())); + } + urlBuilder.Append(sb.ToString()); + + IList> headers = new List>(); + //makeBlockDic.Add("Content-Type", "application/octet-stream"); + + string authHead = "UpToken " + upToken; + headers.Add(new KeyValuePair("Authorization", authHead)); + int proCount = ctxes.Length; + Stream body = new MemoryStream(); + + for (int i = 0; i < proCount; i++) + { + byte[] bctx = StringToAscii(ctxes[i]); + body.Write(bctx, 0, bctx.Length); + if (i != proCount - 1) + { + body.WriteByte((byte)','); + } + } + body.Seek(0, SeekOrigin.Begin); + + var rtn = AVClient.RequestAsync(new Uri(urlBuilder.ToString()), "POST", headers, body, "text/plain", cancellationToken).OnSuccess(_ => + { + var dic = AVClient.ReponseResolve(_.Result, CancellationToken.None); + return _.Result; + }); + return rtn; + } + internal void MergeFromJSON(FileState state, IDictionary jsonData) + { + lock (this.mutex) + { + string url = jsonData["url"] as string; + state.Url = new Uri(url, UriKind.Absolute); + state.bucketId = FetchBucketId(url); + state.token = jsonData["token"] as string; + state.bucket = jsonData["bucket"] as string; + state.ObjectId = jsonData["objectId"] as string; + } + } + + string FetchBucketId(string url) + { + var elements = url.Split('/'); + + return elements[elements.Length - 1]; + } + public static byte[] StringToAscii(string s) + { + byte[] retval = new byte[s.Length]; + for (int ix = 0; ix < s.Length; ++ix) + { + char ch = s[ix]; + if (ch <= 0x7f) + retval[ix] = (byte)ch; + else + retval[ix] = (byte)'?'; + } + return retval; + } + public static string ToBase64URLSafe(string str) + { + return Encode(str); + } + public static string Encode(byte[] bs) + { + if (bs == null || bs.Length == 0) + return ""; + string encodedStr = Convert.ToBase64String(bs); + encodedStr = encodedStr.Replace('+', '-').Replace('/', '_'); + return encodedStr; + } + public static string Encode(string text) + { + if (String.IsNullOrEmpty(text)) + return ""; + byte[] bs = Encoding.UTF8.GetBytes(text); + string encodedStr = Convert.ToBase64String(bs); + encodedStr = encodedStr.Replace('+', '-').Replace('/', '_'); + return encodedStr; + } + + internal static string GetMD5Code(Stream data) + { + MD5 md5 = new MD5CryptoServiceProvider(); + byte[] retVal = md5.ComputeHash(data); + + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < retVal.Length; i++) + { + sb.Append(retVal[i].ToString("x2")); + } + return sb.ToString(); + + } + + internal IDictionary GetMetaData(FileState state, Stream data) + { + IDictionary rtn = new Dictionary(); + + if (state.MetaData != null) + { + foreach (var meta in state.MetaData) + { + rtn.Add(meta.Key, meta.Value); + } + } + MergeDic(rtn, "mime_type", AVFile.GetMIMEType(state.Name)); + MergeDic(rtn, "size", data.Length); + MergeDic(rtn, "_checksum", GetMD5Code(data)); + if (AVUser.CurrentUser != null) + if (AVUser.CurrentUser.ObjectId != null) + MergeDic(rtn, "owner", AVUser.CurrentUser.ObjectId); + + return rtn; + } + internal void MergeDic(IDictionary dic, string key, object value) + { + if (dic.ContainsKey(key)) + { + dic[key] = value; + } + else + { + dic.Add(key, value); + } + } + } +} diff --git a/Storage/Source/Internal/File/Cryptography/MD5/MD5.cs b/Storage/Source/Internal/File/Cryptography/MD5/MD5.cs new file mode 100644 index 0000000..aa3eb2b --- /dev/null +++ b/Storage/Source/Internal/File/Cryptography/MD5/MD5.cs @@ -0,0 +1,566 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.IO; + +namespace LeanCloud.Storage.Internal +{ + /// + /// 弥补Windows Phone 8 API没有自带MD5加密的拓展方法。 + /// + internal class MD5CryptoServiceProvider : MD5 + { + public MD5CryptoServiceProvider() + : base() + { + } + } + /// + /// Summary description for MD5. + /// + internal class MD5 : IDisposable + { + static public MD5 Create(string hashName) + { + if (hashName == "MD5") + return new MD5(); + else + throw new NotSupportedException(); + } + + static public string GetMd5String(String source) + { + MD5 md = MD5CryptoServiceProvider.Create(); + byte[] hash; + + //Create a new instance of ASCIIEncoding to + //convert the string into an array of Unicode bytes. + UTF8Encoding enc = new UTF8Encoding(); + // ASCIIEncoding enc = new ASCIIEncoding(); + + //Convert the string into an array of bytes. + byte[] buffer = enc.GetBytes(source); + + //Create the hash value from the array of bytes. + hash = md.ComputeHash(buffer); + + StringBuilder sb = new StringBuilder(); + foreach (byte b in hash) + sb.Append(b.ToString("x2")); + return sb.ToString(); + } + + static public MD5 Create() + { + return new MD5(); + } + + #region base implementation of the MD5 + #region constants + private const byte S11 = 7; + private const byte S12 = 12; + private const byte S13 = 17; + private const byte S14 = 22; + private const byte S21 = 5; + private const byte S22 = 9; + private const byte S23 = 14; + private const byte S24 = 20; + private const byte S31 = 4; + private const byte S32 = 11; + private const byte S33 = 16; + private const byte S34 = 23; + private const byte S41 = 6; + private const byte S42 = 10; + private const byte S43 = 15; + private const byte S44 = 21; + static private byte[] PADDING = new byte[] { + 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + }; + #endregion + + #region F, G, H and I are basic MD5 functions. + static private uint F(uint x, uint y, uint z) + { + return (((x) & (y)) | ((~x) & (z))); + } + static private uint G(uint x, uint y, uint z) + { + return (((x) & (z)) | ((y) & (~z))); + } + static private uint H(uint x, uint y, uint z) + { + return ((x) ^ (y) ^ (z)); + } + static private uint I(uint x, uint y, uint z) + { + return ((y) ^ ((x) | (~z))); + } + #endregion + + #region rotates x left n bits. + /// + /// rotates x left n bits. + /// + /// + /// + /// + static private uint ROTATE_LEFT(uint x, byte n) + { + return (((x) << (n)) | ((x) >> (32 - (n)))); + } + #endregion + + #region FF, GG, HH, and II transformations + /// FF, GG, HH, and II transformations + /// for rounds 1, 2, 3, and 4. + /// Rotation is separate from addition to prevent recomputation. + static private void FF(ref uint a, uint b, uint c, uint d, uint x, byte s, uint ac) + { + (a) += F((b), (c), (d)) + (x) + (uint)(ac); + (a) = ROTATE_LEFT((a), (s)); + (a) += (b); + } + static private void GG(ref uint a, uint b, uint c, uint d, uint x, byte s, uint ac) + { + (a) += G((b), (c), (d)) + (x) + (uint)(ac); + (a) = ROTATE_LEFT((a), (s)); + (a) += (b); + } + static private void HH(ref uint a, uint b, uint c, uint d, uint x, byte s, uint ac) + { + (a) += H((b), (c), (d)) + (x) + (uint)(ac); + (a) = ROTATE_LEFT((a), (s)); + (a) += (b); + } + static private void II(ref uint a, uint b, uint c, uint d, uint x, byte s, uint ac) + { + (a) += I((b), (c), (d)) + (x) + (uint)(ac); + (a) = ROTATE_LEFT((a), (s)); + (a) += (b); + } + #endregion + + #region context info + /// + /// state (ABCD) + /// + uint[] state = new uint[4]; + + /// + /// number of bits, modulo 2^64 (lsb first) + /// + uint[] count = new uint[2]; + + /// + /// input buffer + /// + byte[] buffer = new byte[64]; + #endregion + + internal MD5() + { + Initialize(); + } + + /// + /// MD5 initialization. Begins an MD5 operation, writing a new context. + /// + /// + /// The RFC named it "MD5Init" + /// + public virtual void Initialize() + { + count[0] = count[1] = 0; + + // Load magic initialization constants. + state[0] = 0x67452301; + state[1] = 0xefcdab89; + state[2] = 0x98badcfe; + state[3] = 0x10325476; + } + + /// + /// MD5 block update operation. Continues an MD5 message-digest + /// operation, processing another message block, and updating the + /// context. + /// + /// + /// + /// + /// The RFC Named it MD5Update + protected virtual void HashCore(byte[] input, int offset, int count) + { + int i; + int index; + int partLen; + + // Compute number of bytes mod 64 + index = (int)((this.count[0] >> 3) & 0x3F); + + // Update number of bits + if ((this.count[0] += (uint)((uint)count << 3)) < ((uint)count << 3)) + this.count[1]++; + this.count[1] += ((uint)count >> 29); + + partLen = 64 - index; + + // Transform as many times as possible. + if (count >= partLen) + { + Buffer.BlockCopy(input, offset, this.buffer, index, partLen); + Transform(this.buffer, 0); + + for (i = partLen; i + 63 < count; i += 64) + Transform(input, offset + i); + + index = 0; + } + else + i = 0; + + // Buffer remaining input + Buffer.BlockCopy(input, offset + i, this.buffer, index, count - i); + } + + /// + /// MD5 finalization. Ends an MD5 message-digest operation, writing the + /// the message digest and zeroizing the context. + /// + /// message digest + /// The RFC named it MD5Final + protected virtual byte[] HashFinal() + { + byte[] digest = new byte[16]; + byte[] bits = new byte[8]; + int index, padLen; + + // Save number of bits + Encode(bits, 0, this.count, 0, 8); + + // Pad out to 56 mod 64. + index = (int)((uint)(this.count[0] >> 3) & 0x3f); + padLen = (index < 56) ? (56 - index) : (120 - index); + HashCore(PADDING, 0, padLen); + + // Append length (before padding) + HashCore(bits, 0, 8); + + // Store state in digest + Encode(digest, 0, state, 0, 16); + + // Zeroize sensitive information. + count[0] = count[1] = 0; + state[0] = 0; + state[1] = 0; + state[2] = 0; + state[3] = 0; + + // initialize again, to be ready to use + Initialize(); + + return digest; + } + + /// + /// MD5 basic transformation. Transforms state based on 64 bytes block. + /// + /// + /// + private void Transform(byte[] block, int offset) + { + uint a = state[0], b = state[1], c = state[2], d = state[3]; + uint[] x = new uint[16]; + Decode(x, 0, block, offset, 64); + + // Round 1 + FF(ref a, b, c, d, x[0], S11, 0xd76aa478); /* 1 */ + FF(ref d, a, b, c, x[1], S12, 0xe8c7b756); /* 2 */ + FF(ref c, d, a, b, x[2], S13, 0x242070db); /* 3 */ + FF(ref b, c, d, a, x[3], S14, 0xc1bdceee); /* 4 */ + FF(ref a, b, c, d, x[4], S11, 0xf57c0faf); /* 5 */ + FF(ref d, a, b, c, x[5], S12, 0x4787c62a); /* 6 */ + FF(ref c, d, a, b, x[6], S13, 0xa8304613); /* 7 */ + FF(ref b, c, d, a, x[7], S14, 0xfd469501); /* 8 */ + FF(ref a, b, c, d, x[8], S11, 0x698098d8); /* 9 */ + FF(ref d, a, b, c, x[9], S12, 0x8b44f7af); /* 10 */ + FF(ref c, d, a, b, x[10], S13, 0xffff5bb1); /* 11 */ + FF(ref b, c, d, a, x[11], S14, 0x895cd7be); /* 12 */ + FF(ref a, b, c, d, x[12], S11, 0x6b901122); /* 13 */ + FF(ref d, a, b, c, x[13], S12, 0xfd987193); /* 14 */ + FF(ref c, d, a, b, x[14], S13, 0xa679438e); /* 15 */ + FF(ref b, c, d, a, x[15], S14, 0x49b40821); /* 16 */ + + // Round 2 + GG(ref a, b, c, d, x[1], S21, 0xf61e2562); /* 17 */ + GG(ref d, a, b, c, x[6], S22, 0xc040b340); /* 18 */ + GG(ref c, d, a, b, x[11], S23, 0x265e5a51); /* 19 */ + GG(ref b, c, d, a, x[0], S24, 0xe9b6c7aa); /* 20 */ + GG(ref a, b, c, d, x[5], S21, 0xd62f105d); /* 21 */ + GG(ref d, a, b, c, x[10], S22, 0x2441453); /* 22 */ + GG(ref c, d, a, b, x[15], S23, 0xd8a1e681); /* 23 */ + GG(ref b, c, d, a, x[4], S24, 0xe7d3fbc8); /* 24 */ + GG(ref a, b, c, d, x[9], S21, 0x21e1cde6); /* 25 */ + GG(ref d, a, b, c, x[14], S22, 0xc33707d6); /* 26 */ + GG(ref c, d, a, b, x[3], S23, 0xf4d50d87); /* 27 */ + GG(ref b, c, d, a, x[8], S24, 0x455a14ed); /* 28 */ + GG(ref a, b, c, d, x[13], S21, 0xa9e3e905); /* 29 */ + GG(ref d, a, b, c, x[2], S22, 0xfcefa3f8); /* 30 */ + GG(ref c, d, a, b, x[7], S23, 0x676f02d9); /* 31 */ + GG(ref b, c, d, a, x[12], S24, 0x8d2a4c8a); /* 32 */ + + // Round 3 + HH(ref a, b, c, d, x[5], S31, 0xfffa3942); /* 33 */ + HH(ref d, a, b, c, x[8], S32, 0x8771f681); /* 34 */ + HH(ref c, d, a, b, x[11], S33, 0x6d9d6122); /* 35 */ + HH(ref b, c, d, a, x[14], S34, 0xfde5380c); /* 36 */ + HH(ref a, b, c, d, x[1], S31, 0xa4beea44); /* 37 */ + HH(ref d, a, b, c, x[4], S32, 0x4bdecfa9); /* 38 */ + HH(ref c, d, a, b, x[7], S33, 0xf6bb4b60); /* 39 */ + HH(ref b, c, d, a, x[10], S34, 0xbebfbc70); /* 40 */ + HH(ref a, b, c, d, x[13], S31, 0x289b7ec6); /* 41 */ + HH(ref d, a, b, c, x[0], S32, 0xeaa127fa); /* 42 */ + HH(ref c, d, a, b, x[3], S33, 0xd4ef3085); /* 43 */ + HH(ref b, c, d, a, x[6], S34, 0x4881d05); /* 44 */ + HH(ref a, b, c, d, x[9], S31, 0xd9d4d039); /* 45 */ + HH(ref d, a, b, c, x[12], S32, 0xe6db99e5); /* 46 */ + HH(ref c, d, a, b, x[15], S33, 0x1fa27cf8); /* 47 */ + HH(ref b, c, d, a, x[2], S34, 0xc4ac5665); /* 48 */ + + // Round 4 + II(ref a, b, c, d, x[0], S41, 0xf4292244); /* 49 */ + II(ref d, a, b, c, x[7], S42, 0x432aff97); /* 50 */ + II(ref c, d, a, b, x[14], S43, 0xab9423a7); /* 51 */ + II(ref b, c, d, a, x[5], S44, 0xfc93a039); /* 52 */ + II(ref a, b, c, d, x[12], S41, 0x655b59c3); /* 53 */ + II(ref d, a, b, c, x[3], S42, 0x8f0ccc92); /* 54 */ + II(ref c, d, a, b, x[10], S43, 0xffeff47d); /* 55 */ + II(ref b, c, d, a, x[1], S44, 0x85845dd1); /* 56 */ + II(ref a, b, c, d, x[8], S41, 0x6fa87e4f); /* 57 */ + II(ref d, a, b, c, x[15], S42, 0xfe2ce6e0); /* 58 */ + II(ref c, d, a, b, x[6], S43, 0xa3014314); /* 59 */ + II(ref b, c, d, a, x[13], S44, 0x4e0811a1); /* 60 */ + II(ref a, b, c, d, x[4], S41, 0xf7537e82); /* 61 */ + II(ref d, a, b, c, x[11], S42, 0xbd3af235); /* 62 */ + II(ref c, d, a, b, x[2], S43, 0x2ad7d2bb); /* 63 */ + II(ref b, c, d, a, x[9], S44, 0xeb86d391); /* 64 */ + + state[0] += a; + state[1] += b; + state[2] += c; + state[3] += d; + + // Zeroize sensitive information. + for (int i = 0; i < x.Length; i++) + x[i] = 0; + } + + /// + /// Encodes input (uint) into output (byte). Assumes len is + /// multiple of 4. + /// + /// + /// + /// + /// + /// + private static void Encode(byte[] output, int outputOffset, uint[] input, int inputOffset, int count) + { + int i, j; + int end = outputOffset + count; + for (i = inputOffset, j = outputOffset; j < end; i++, j += 4) + { + output[j] = (byte)(input[i] & 0xff); + output[j + 1] = (byte)((input[i] >> 8) & 0xff); + output[j + 2] = (byte)((input[i] >> 16) & 0xff); + output[j + 3] = (byte)((input[i] >> 24) & 0xff); + } + } + + /// + /// Decodes input (byte) into output (uint). Assumes len is + /// a multiple of 4. + /// + /// + /// + /// + /// + /// + static private void Decode(uint[] output, int outputOffset, byte[] input, int inputOffset, int count) + { + int i, j; + int end = inputOffset + count; + for (i = outputOffset, j = inputOffset; j < end; i++, j += 4) + output[i] = ((uint)input[j]) | (((uint)input[j + 1]) << 8) | (((uint)input[j + 2]) << 16) | (((uint)input[j + 3]) << 24); + } + #endregion + + #region expose the same interface as the regular MD5 object + + protected byte[] HashValue; + protected int State; + public virtual bool CanReuseTransform + { + get + { + return true; + } + } + + public virtual bool CanTransformMultipleBlocks + { + get + { + return true; + } + } + public virtual byte[] Hash + { + get + { + if (this.State != 0) + throw new InvalidOperationException(); + return (byte[])HashValue.Clone(); + } + } + public virtual int HashSize + { + get + { + return HashSizeValue; + } + } + protected int HashSizeValue = 128; + + public virtual int InputBlockSize + { + get + { + return 1; + } + } + public virtual int OutputBlockSize + { + get + { + return 1; + } + } + + public void Clear() + { + Dispose(true); + } + + public byte[] ComputeHash(byte[] buffer) + { + return ComputeHash(buffer, 0, buffer.Length); + } + public byte[] ComputeHash(byte[] buffer, int offset, int count) + { + Initialize(); + HashCore(buffer, offset, count); + HashValue = HashFinal(); + return (byte[])HashValue.Clone(); + } + + public byte[] ComputeHash(Stream inputStream) + { + Initialize(); + int count; + byte[] buffer = new byte[4096]; + while (0 < (count = inputStream.Read(buffer, 0, 4096))) + { + HashCore(buffer, 0, count); + } + HashValue = HashFinal(); + return (byte[])HashValue.Clone(); + } + + public int TransformBlock( + byte[] inputBuffer, + int inputOffset, + int inputCount, + byte[] outputBuffer, + int outputOffset + ) + { + if (inputBuffer == null) + { + throw new ArgumentNullException("inputBuffer"); + } + if (inputOffset < 0) + { + throw new ArgumentOutOfRangeException("inputOffset"); + } + if ((inputCount < 0) || (inputCount > inputBuffer.Length)) + { + throw new ArgumentException("inputCount"); + } + if ((inputBuffer.Length - inputCount) < inputOffset) + { + throw new ArgumentOutOfRangeException("inputOffset"); + } + if (this.State == 0) + { + Initialize(); + this.State = 1; + } + + HashCore(inputBuffer, inputOffset, inputCount); + if ((inputBuffer != outputBuffer) || (inputOffset != outputOffset)) + { + Buffer.BlockCopy(inputBuffer, inputOffset, outputBuffer, outputOffset, inputCount); + } + return inputCount; + } + public byte[] TransformFinalBlock( + byte[] inputBuffer, + int inputOffset, + int inputCount + ) + { + if (inputBuffer == null) + { + throw new ArgumentNullException("inputBuffer"); + } + if (inputOffset < 0) + { + throw new ArgumentOutOfRangeException("inputOffset"); + } + if ((inputCount < 0) || (inputCount > inputBuffer.Length)) + { + throw new ArgumentException("inputCount"); + } + if ((inputBuffer.Length - inputCount) < inputOffset) + { + throw new ArgumentOutOfRangeException("inputOffset"); + } + if (this.State == 0) + { + Initialize(); + } + HashCore(inputBuffer, inputOffset, inputCount); + HashValue = HashFinal(); + byte[] buffer = new byte[inputCount]; + Buffer.BlockCopy(inputBuffer, inputOffset, buffer, 0, inputCount); + this.State = 0; + return buffer; + } + #endregion + + protected virtual void Dispose(bool disposing) + { + if (!disposing) + Initialize(); + } + public void Dispose() + { + Dispose(true); + } + } +} + diff --git a/Storage/Source/Internal/File/Cryptography/SHA1/SHA1CryptoServiceProvider.cs b/Storage/Source/Internal/File/Cryptography/SHA1/SHA1CryptoServiceProvider.cs new file mode 100644 index 0000000..74f6e09 --- /dev/null +++ b/Storage/Source/Internal/File/Cryptography/SHA1/SHA1CryptoServiceProvider.cs @@ -0,0 +1,495 @@ +// +// System.Security.Cryptography.SHA1CryptoServiceProvider.cs +// +// Authors: +// Matthew S. Ford (Matthew.S.Ford@Rose-Hulman.Edu) +// Sebastien Pouliot (sebastien@ximian.com) +// +// Copyright 2001 by Matthew S. Ford. +// Copyright (C) 2004, 2005, 2008 Novell, Inc (http://www.novell.com) +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +// Note: +// The MS Framework includes two (almost) identical class for SHA1. +// SHA1Managed is a 100% managed implementation. +// SHA1CryptoServiceProvider (this file) is a wrapper on CryptoAPI. +// Mono must provide those two class for binary compatibility. +// In our case both class are wrappers around a managed internal class SHA1Internal. + +using System.IO; +using System; +using System.Runtime.InteropServices; + +namespace LeanCloud.Storage.Internal +{ + + internal class SHA1Internal + { + + private const int BLOCK_SIZE_BYTES = 64; + private uint[] _H; // these are my chaining variables + private ulong count; + private byte[] _ProcessingBuffer; // Used to start data when passed less than a block worth. + private int _ProcessingBufferCount; // Counts how much data we have stored that still needs processed. + private uint[] buff; + + public SHA1Internal() + { + _H = new uint[5]; + _ProcessingBuffer = new byte[BLOCK_SIZE_BYTES]; + buff = new uint[80]; + + Initialize(); + } + + public void HashCore(byte[] rgb, int ibStart, int cbSize) + { + int i; + + if (_ProcessingBufferCount != 0) + { + if (cbSize < (BLOCK_SIZE_BYTES - _ProcessingBufferCount)) + { + System.Buffer.BlockCopy(rgb, ibStart, _ProcessingBuffer, _ProcessingBufferCount, cbSize); + _ProcessingBufferCount += cbSize; + return; + } + else + { + i = (BLOCK_SIZE_BYTES - _ProcessingBufferCount); + System.Buffer.BlockCopy(rgb, ibStart, _ProcessingBuffer, _ProcessingBufferCount, i); + ProcessBlock(_ProcessingBuffer, 0); + _ProcessingBufferCount = 0; + ibStart += i; + cbSize -= i; + } + } + + for (i = 0; i < cbSize - cbSize % BLOCK_SIZE_BYTES; i += BLOCK_SIZE_BYTES) + { + ProcessBlock(rgb, (uint)(ibStart + i)); + } + + if (cbSize % BLOCK_SIZE_BYTES != 0) + { + System.Buffer.BlockCopy(rgb, cbSize - cbSize % BLOCK_SIZE_BYTES + ibStart, _ProcessingBuffer, 0, cbSize % BLOCK_SIZE_BYTES); + _ProcessingBufferCount = cbSize % BLOCK_SIZE_BYTES; + } + } + + public byte[] HashFinal() + { + byte[] hash = new byte[20]; + + ProcessFinalBlock(_ProcessingBuffer, 0, _ProcessingBufferCount); + + for (int i = 0; i < 5; i++) + { + for (int j = 0; j < 4; j++) + { + hash[i * 4 + j] = (byte)(_H[i] >> (8 * (3 - j))); + } + } + + return hash; + } + + public void Initialize() + { + count = 0; + _ProcessingBufferCount = 0; + + _H[0] = 0x67452301; + _H[1] = 0xefcdab89; + _H[2] = 0x98badcfe; + _H[3] = 0x10325476; + _H[4] = 0xC3D2E1F0; + } + + private void ProcessBlock(byte[] inputBuffer, uint inputOffset) + { + uint a, b, c, d, e; + + count += BLOCK_SIZE_BYTES; + + // abc removal would not work on the fields + uint[] _H = this._H; + uint[] buff = this.buff; + InitialiseBuff(buff, inputBuffer, inputOffset); + FillBuff(buff); + + a = _H[0]; + b = _H[1]; + c = _H[2]; + d = _H[3]; + e = _H[4]; + + // This function was unrolled because it seems to be doubling our performance with current compiler/VM. + // Possibly roll up if this changes. + + // ---- Round 1 -------- + int i = 0; + while (i < 20) + { + e += ((a << 5) | (a >> 27)) + (((c ^ d) & b) ^ d) + 0x5A827999 + buff[i]; + b = (b << 30) | (b >> 2); + + d += ((e << 5) | (e >> 27)) + (((b ^ c) & a) ^ c) + 0x5A827999 + buff[i + 1]; + a = (a << 30) | (a >> 2); + + c += ((d << 5) | (d >> 27)) + (((a ^ b) & e) ^ b) + 0x5A827999 + buff[i + 2]; + e = (e << 30) | (e >> 2); + + b += ((c << 5) | (c >> 27)) + (((e ^ a) & d) ^ a) + 0x5A827999 + buff[i + 3]; + d = (d << 30) | (d >> 2); + + a += ((b << 5) | (b >> 27)) + (((d ^ e) & c) ^ e) + 0x5A827999 + buff[i + 4]; + c = (c << 30) | (c >> 2); + i += 5; + } + + // ---- Round 2 -------- + while (i < 40) + { + e += ((a << 5) | (a >> 27)) + (b ^ c ^ d) + 0x6ED9EBA1 + buff[i]; + b = (b << 30) | (b >> 2); + + d += ((e << 5) | (e >> 27)) + (a ^ b ^ c) + 0x6ED9EBA1 + buff[i + 1]; + a = (a << 30) | (a >> 2); + + c += ((d << 5) | (d >> 27)) + (e ^ a ^ b) + 0x6ED9EBA1 + buff[i + 2]; + e = (e << 30) | (e >> 2); + + b += ((c << 5) | (c >> 27)) + (d ^ e ^ a) + 0x6ED9EBA1 + buff[i + 3]; + d = (d << 30) | (d >> 2); + + a += ((b << 5) | (b >> 27)) + (c ^ d ^ e) + 0x6ED9EBA1 + buff[i + 4]; + c = (c << 30) | (c >> 2); + i += 5; + } + + // ---- Round 3 -------- + while (i < 60) + { + e += ((a << 5) | (a >> 27)) + ((b & c) | (b & d) | (c & d)) + 0x8F1BBCDC + buff[i]; + b = (b << 30) | (b >> 2); + + d += ((e << 5) | (e >> 27)) + ((a & b) | (a & c) | (b & c)) + 0x8F1BBCDC + buff[i + 1]; + a = (a << 30) | (a >> 2); + + c += ((d << 5) | (d >> 27)) + ((e & a) | (e & b) | (a & b)) + 0x8F1BBCDC + buff[i + 2]; + e = (e << 30) | (e >> 2); + + b += ((c << 5) | (c >> 27)) + ((d & e) | (d & a) | (e & a)) + 0x8F1BBCDC + buff[i + 3]; + d = (d << 30) | (d >> 2); + + a += ((b << 5) | (b >> 27)) + ((c & d) | (c & e) | (d & e)) + 0x8F1BBCDC + buff[i + 4]; + c = (c << 30) | (c >> 2); + i += 5; + } + + // ---- Round 4 -------- + while (i < 80) + { + e += ((a << 5) | (a >> 27)) + (b ^ c ^ d) + 0xCA62C1D6 + buff[i]; + b = (b << 30) | (b >> 2); + + d += ((e << 5) | (e >> 27)) + (a ^ b ^ c) + 0xCA62C1D6 + buff[i + 1]; + a = (a << 30) | (a >> 2); + + c += ((d << 5) | (d >> 27)) + (e ^ a ^ b) + 0xCA62C1D6 + buff[i + 2]; + e = (e << 30) | (e >> 2); + + b += ((c << 5) | (c >> 27)) + (d ^ e ^ a) + 0xCA62C1D6 + buff[i + 3]; + d = (d << 30) | (d >> 2); + + a += ((b << 5) | (b >> 27)) + (c ^ d ^ e) + 0xCA62C1D6 + buff[i + 4]; + c = (c << 30) | (c >> 2); + i += 5; + } + + _H[0] += a; + _H[1] += b; + _H[2] += c; + _H[3] += d; + _H[4] += e; + } + + private static void InitialiseBuff(uint[] buff, byte[] input, uint inputOffset) + { + buff[0] = (uint)((input[inputOffset + 0] << 24) | (input[inputOffset + 1] << 16) | (input[inputOffset + 2] << 8) | (input[inputOffset + 3])); + buff[1] = (uint)((input[inputOffset + 4] << 24) | (input[inputOffset + 5] << 16) | (input[inputOffset + 6] << 8) | (input[inputOffset + 7])); + buff[2] = (uint)((input[inputOffset + 8] << 24) | (input[inputOffset + 9] << 16) | (input[inputOffset + 10] << 8) | (input[inputOffset + 11])); + buff[3] = (uint)((input[inputOffset + 12] << 24) | (input[inputOffset + 13] << 16) | (input[inputOffset + 14] << 8) | (input[inputOffset + 15])); + buff[4] = (uint)((input[inputOffset + 16] << 24) | (input[inputOffset + 17] << 16) | (input[inputOffset + 18] << 8) | (input[inputOffset + 19])); + buff[5] = (uint)((input[inputOffset + 20] << 24) | (input[inputOffset + 21] << 16) | (input[inputOffset + 22] << 8) | (input[inputOffset + 23])); + buff[6] = (uint)((input[inputOffset + 24] << 24) | (input[inputOffset + 25] << 16) | (input[inputOffset + 26] << 8) | (input[inputOffset + 27])); + buff[7] = (uint)((input[inputOffset + 28] << 24) | (input[inputOffset + 29] << 16) | (input[inputOffset + 30] << 8) | (input[inputOffset + 31])); + buff[8] = (uint)((input[inputOffset + 32] << 24) | (input[inputOffset + 33] << 16) | (input[inputOffset + 34] << 8) | (input[inputOffset + 35])); + buff[9] = (uint)((input[inputOffset + 36] << 24) | (input[inputOffset + 37] << 16) | (input[inputOffset + 38] << 8) | (input[inputOffset + 39])); + buff[10] = (uint)((input[inputOffset + 40] << 24) | (input[inputOffset + 41] << 16) | (input[inputOffset + 42] << 8) | (input[inputOffset + 43])); + buff[11] = (uint)((input[inputOffset + 44] << 24) | (input[inputOffset + 45] << 16) | (input[inputOffset + 46] << 8) | (input[inputOffset + 47])); + buff[12] = (uint)((input[inputOffset + 48] << 24) | (input[inputOffset + 49] << 16) | (input[inputOffset + 50] << 8) | (input[inputOffset + 51])); + buff[13] = (uint)((input[inputOffset + 52] << 24) | (input[inputOffset + 53] << 16) | (input[inputOffset + 54] << 8) | (input[inputOffset + 55])); + buff[14] = (uint)((input[inputOffset + 56] << 24) | (input[inputOffset + 57] << 16) | (input[inputOffset + 58] << 8) | (input[inputOffset + 59])); + buff[15] = (uint)((input[inputOffset + 60] << 24) | (input[inputOffset + 61] << 16) | (input[inputOffset + 62] << 8) | (input[inputOffset + 63])); + } + + private static void FillBuff(uint[] buff) + { + uint val; + for (int i = 16; i < 80; i += 8) + { + val = buff[i - 3] ^ buff[i - 8] ^ buff[i - 14] ^ buff[i - 16]; + buff[i] = (val << 1) | (val >> 31); + + val = buff[i - 2] ^ buff[i - 7] ^ buff[i - 13] ^ buff[i - 15]; + buff[i + 1] = (val << 1) | (val >> 31); + + val = buff[i - 1] ^ buff[i - 6] ^ buff[i - 12] ^ buff[i - 14]; + buff[i + 2] = (val << 1) | (val >> 31); + + val = buff[i + 0] ^ buff[i - 5] ^ buff[i - 11] ^ buff[i - 13]; + buff[i + 3] = (val << 1) | (val >> 31); + + val = buff[i + 1] ^ buff[i - 4] ^ buff[i - 10] ^ buff[i - 12]; + buff[i + 4] = (val << 1) | (val >> 31); + + val = buff[i + 2] ^ buff[i - 3] ^ buff[i - 9] ^ buff[i - 11]; + buff[i + 5] = (val << 1) | (val >> 31); + + val = buff[i + 3] ^ buff[i - 2] ^ buff[i - 8] ^ buff[i - 10]; + buff[i + 6] = (val << 1) | (val >> 31); + + val = buff[i + 4] ^ buff[i - 1] ^ buff[i - 7] ^ buff[i - 9]; + buff[i + 7] = (val << 1) | (val >> 31); + } + } + + private void ProcessFinalBlock(byte[] inputBuffer, int inputOffset, int inputCount) + { + ulong total = count + (ulong)inputCount; + int paddingSize = (56 - (int)(total % BLOCK_SIZE_BYTES)); + + if (paddingSize < 1) + paddingSize += BLOCK_SIZE_BYTES; + + int length = inputCount + paddingSize + 8; + byte[] fooBuffer = (length == 64) ? _ProcessingBuffer : new byte[length]; + + for (int i = 0; i < inputCount; i++) + { + fooBuffer[i] = inputBuffer[i + inputOffset]; + } + + fooBuffer[inputCount] = 0x80; + for (int i = inputCount + 1; i < inputCount + paddingSize; i++) + { + fooBuffer[i] = 0x00; + } + + // I deal in bytes. The algorithm deals in bits. + ulong size = total << 3; + AddLength(size, fooBuffer, inputCount + paddingSize); + ProcessBlock(fooBuffer, 0); + + if (length == 128) + ProcessBlock(fooBuffer, 64); + } + + internal void AddLength(ulong length, byte[] buffer, int position) + { + buffer[position++] = (byte)(length >> 56); + buffer[position++] = (byte)(length >> 48); + buffer[position++] = (byte)(length >> 40); + buffer[position++] = (byte)(length >> 32); + buffer[position++] = (byte)(length >> 24); + buffer[position++] = (byte)(length >> 16); + buffer[position++] = (byte)(length >> 8); + buffer[position] = (byte)(length); + } + } + + public sealed class SHA1CryptoServiceProvider : SHA1 + { + + private SHA1Internal sha; + + public SHA1CryptoServiceProvider() + { + sha = new SHA1Internal(); + } + + ~SHA1CryptoServiceProvider() + { + Dispose(false); + } + + protected override void Dispose(bool disposing) + { + // nothing new to do (managed implementation) + base.Dispose(disposing); + } + + protected override void HashCore(byte[] rgb, int ibStart, int cbSize) + { + State = 1; + sha.HashCore(rgb, ibStart, cbSize); + } + + protected override byte[] HashFinal() + { + State = 0; + return sha.HashFinal(); + } + + public override void Initialize() + { + sha.Initialize(); + } + } + + public abstract class SHA1 : HashAlgorithm + { + protected SHA1() + { + HashSizeValue = 160; + } + } + + public abstract class HashAlgorithm : IDisposable + { + protected int HashSizeValue; + protected internal byte[] HashValue; + protected int State = 0; + + private bool m_bDisposed = false; + + protected HashAlgorithm() { } + + // + // public properties + // + + public virtual int HashSize + { + get { return HashSizeValue; } + } + + // + // public methods + // + + public byte[] ComputeHash(Stream inputStream) + { + if (m_bDisposed) + throw new ObjectDisposedException(null); + + // Default the buffer size to 4K. + byte[] buffer = new byte[4096]; + int bytesRead; + do + { + bytesRead = inputStream.Read(buffer, 0, 4096); + if (bytesRead > 0) + { + HashCore(buffer, 0, bytesRead); + } + } while (bytesRead > 0); + + HashValue = HashFinal(); + byte[] Tmp = (byte[])HashValue.Clone(); + Initialize(); + return (Tmp); + } + + public byte[] ComputeHash(byte[] buffer) + { + if (m_bDisposed) + throw new ObjectDisposedException(null); + + // Do some validation + if (buffer == null) throw new ArgumentNullException("buffer"); + + HashCore(buffer, 0, buffer.Length); + HashValue = HashFinal(); + byte[] Tmp = (byte[])HashValue.Clone(); + Initialize(); + return (Tmp); + } + + // ICryptoTransform methods + + // we assume any HashAlgorithm can take input a byte at a time + public virtual int InputBlockSize + { + get { return (1); } + } + + public virtual int OutputBlockSize + { + get { return (1); } + } + + public virtual bool CanTransformMultipleBlocks + { + get { return (true); } + } + + public virtual bool CanReuseTransform + { + get { return (true); } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + public void Clear() + { + (this as IDisposable).Dispose(); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + if (HashValue != null) + Array.Clear(HashValue, 0, HashValue.Length); + HashValue = null; + m_bDisposed = true; + } + } + + // + // abstract public methods + // + + public abstract void Initialize(); + + protected abstract void HashCore(byte[] array, int ibStart, int cbSize); + + protected abstract byte[] HashFinal(); + } +} diff --git a/Storage/Source/Internal/File/State/FileState.cs b/Storage/Source/Internal/File/State/FileState.cs new file mode 100644 index 0000000..667646a --- /dev/null +++ b/Storage/Source/Internal/File/State/FileState.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace LeanCloud.Storage.Internal +{ + public class FileState + { + public string ObjectId { get; internal set; } + public string Name { get; internal set; } + public string CloudName { get; set; } + public string MimeType { get; internal set; } + public Uri Url { get; internal set; } + public IDictionary MetaData { get; internal set; } + public long Size { get; internal set; } + public long FixedChunkSize { get; internal set; } + + public int counter; + public Stream frozenData; + public string bucketId; + public string bucket; + public string token; + public long completed; + public List block_ctxes = new List(); + + } +} diff --git a/Storage/Source/Internal/HttpClient/HttpRequest.cs b/Storage/Source/Internal/HttpClient/HttpRequest.cs new file mode 100644 index 0000000..f3e1444 --- /dev/null +++ b/Storage/Source/Internal/HttpClient/HttpRequest.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace LeanCloud.Storage.Internal +{ + /// + /// IHttpRequest is an interface that provides an API to execute HTTP request data. + /// + public class HttpRequest + { + public Uri Uri { get; set; } + public IList> Headers { get; set; } + + /// + /// Data stream to be uploaded. + /// + public virtual Stream Data { get; set; } + + /// + /// HTTP method. One of DELETE, GET, HEAD, POST or PUT + /// + public string Method { get; set; } + } +} diff --git a/Storage/Source/Internal/HttpClient/IHttpClient.cs b/Storage/Source/Internal/HttpClient/IHttpClient.cs new file mode 100644 index 0000000..6bbe9d2 --- /dev/null +++ b/Storage/Source/Internal/HttpClient/IHttpClient.cs @@ -0,0 +1,24 @@ +using System; +using System.Net; +using System.Threading; +using System.Threading.Tasks; + +namespace LeanCloud.Storage.Internal +{ + public interface IHttpClient + { + /// + /// Executes HTTP request to a with HTTP verb + /// and . + /// + /// The HTTP request to be executed. + /// Upload progress callback. + /// Download progress callback. + /// The cancellation token. + /// A task that resolves to Htt + Task> ExecuteAsync(HttpRequest httpRequest, + IProgress uploadProgress, + IProgress downloadProgress, + CancellationToken cancellationToken); + } +} diff --git a/Storage/Source/Internal/HttpClient/Portable/HttpClient.Portable.cs b/Storage/Source/Internal/HttpClient/Portable/HttpClient.Portable.cs new file mode 100644 index 0000000..210b66f --- /dev/null +++ b/Storage/Source/Internal/HttpClient/Portable/HttpClient.Portable.cs @@ -0,0 +1,163 @@ +using System; +using System.IO; +using System.Net; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Net.Http; +using System.Linq; + +using NetHttpClient = System.Net.Http.HttpClient; +using System.Net.Http.Headers; +using System.Collections.Generic; + +namespace LeanCloud.Storage.Internal +{ + public class HttpClient : IHttpClient + { + private static HashSet HttpContentHeaders = new HashSet { + { "Allow" }, + { "Content-Disposition" }, + { "Content-Encoding" }, + { "Content-Language" }, + { "Content-Length" }, + { "Content-Location" }, + { "Content-MD5" }, + { "Content-Range" }, + { "Content-Type" }, + { "Expires" }, + { "Last-Modified" } + }; + + public HttpClient() + { + client = new NetHttpClient(); + client.DefaultRequestHeaders.Add("User-Agent", "LeanCloud-dotNet-SDK/" + "2.0.0"); + } + + public HttpClient(NetHttpClient client) + { + this.client = client; + } + + private NetHttpClient client; + + public Task> ExecuteAsync(HttpRequest httpRequest, + IProgress uploadProgress, + IProgress downloadProgress, + CancellationToken cancellationToken) + { + uploadProgress = uploadProgress ?? new Progress(); + downloadProgress = downloadProgress ?? new Progress(); + + HttpMethod httpMethod = new HttpMethod(httpRequest.Method); + HttpRequestMessage message = new HttpRequestMessage(httpMethod, httpRequest.Uri); + + // Fill in zero-length data if method is post. + Stream data = httpRequest.Data; + if (httpRequest.Data == null && httpRequest.Method.ToLower().Equals("post")) + { + data = new MemoryStream(new byte[0]); + } + + if (data != null) + { + message.Content = new StreamContent(data); + } + + if (httpRequest.Headers != null) + { + foreach (var header in httpRequest.Headers) + { + if (!string.IsNullOrEmpty(header.Value)) + { + if (HttpContentHeaders.Contains(header.Key)) + { + message.Content.Headers.Add(header.Key, header.Value); + } + else + { + message.Headers.Add(header.Key, header.Value); + } + } + } + } + + // Avoid aggressive caching on Windows Phone 8.1. + message.Headers.Add("Cache-Control", "no-cache"); + message.Headers.IfModifiedSince = DateTimeOffset.UtcNow; + + // TODO: (richardross) investigate progress here, maybe there's something we're missing in order to support this. + uploadProgress.Report(new AVUploadProgressEventArgs { Progress = 0 }); + + return client.SendAsync(message, HttpCompletionOption.ResponseHeadersRead, cancellationToken) + .ContinueWith(httpMessageTask => + { + var response = httpMessageTask.Result; + + uploadProgress.Report(new AVUploadProgressEventArgs { Progress = 1 }); + + return response.Content.ReadAsStreamAsync().ContinueWith(streamTask => + { + var resultStream = new MemoryStream(); + var responseStream = streamTask.Result; + + int bufferSize = 4096; + byte[] buffer = new byte[bufferSize]; + int bytesRead = 0; + long totalLength = -1; + long readSoFar = 0; + + try + { + totalLength = responseStream.Length; + } + catch (NotSupportedException) + { + + } + + return InternalExtensions.WhileAsync(() => + { + return responseStream.ReadAsync(buffer, 0, bufferSize, cancellationToken).OnSuccess(readTask => + { + bytesRead = readTask.Result; + return bytesRead > 0; + }); + }, () => + { + cancellationToken.ThrowIfCancellationRequested(); + + return resultStream.WriteAsync(buffer, 0, bytesRead, cancellationToken).OnSuccess(_ => + { + cancellationToken.ThrowIfCancellationRequested(); + readSoFar += bytesRead; + + if (totalLength > -1) + { + downloadProgress.Report(new AVDownloadProgressEventArgs { Progress = 1.0 * readSoFar / totalLength }); + } + }); + }).ContinueWith(_ => + { + responseStream.Dispose(); + return _; + }).Unwrap().OnSuccess(_ => + { + // If getting stream size is not supported, then report download only once. + if (totalLength == -1) + { + downloadProgress.Report(new AVDownloadProgressEventArgs { Progress = 1.0 }); + } + + // Assume UTF-8 encoding. + var resultAsArray = resultStream.ToArray(); + var resultString = Encoding.UTF8.GetString(resultAsArray, 0, resultAsArray.Length); + resultStream.Dispose(); + return new Tuple(response.StatusCode, resultString); + }); + }); + }).Unwrap().Unwrap(); + } + } +} diff --git a/Storage/Source/Internal/HttpClient/Unity/HttpClient.Unity.cs b/Storage/Source/Internal/HttpClient/Unity/HttpClient.Unity.cs new file mode 100644 index 0000000..adf0ce2 --- /dev/null +++ b/Storage/Source/Internal/HttpClient/Unity/HttpClient.Unity.cs @@ -0,0 +1,168 @@ +using System; +using System.IO; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using UnityEngine; +using UnityEngine.Networking; + +namespace LeanCloud.Storage.Internal +{ + public class HttpClient : IHttpClient + { + private static bool isCompiledByIL2CPP = System.AppDomain.CurrentDomain.FriendlyName.Equals("IL2CPP Root Domain"); + + public Task> ExecuteAsync(HttpRequest httpRequest, + IProgress uploadProgress, + IProgress downloadProgress, + CancellationToken cancellationToken) + { + var tcs = new TaskCompletionSource>(); + cancellationToken.Register(() => tcs.TrySetCanceled()); + uploadProgress = uploadProgress ?? new Progress(); + downloadProgress = downloadProgress ?? new Progress(); + + Task readBytesTask = null; + IDisposable toDisposeAfterReading = null; + byte[] bytes = null; + if (httpRequest.Data != null) + { + var ms = new MemoryStream(); + toDisposeAfterReading = ms; + readBytesTask = httpRequest.Data.CopyToAsync(ms).OnSuccess(_ => + { + bytes = ms.ToArray(); + }); + } + + readBytesTask.Safe().ContinueWith(t => + { + if (toDisposeAfterReading != null) + { + toDisposeAfterReading.Dispose(); + } + return t; + }).Unwrap().OnSuccess(_ => + { + float oldDownloadProgress = 0; + float oldUploadProgress = 0; + + Dispatcher.Instance.Post(() => + { + WaitForWebRequest(GenerateRequest(httpRequest, bytes), request => + { + if (cancellationToken.IsCancellationRequested) + { + tcs.TrySetCanceled(); + return; + } + if (request.isDone) + { + uploadProgress.Report(new AVUploadProgressEventArgs { Progress = 1 }); + downloadProgress.Report(new AVDownloadProgressEventArgs { Progress = 1 }); + + var statusCode = GetResponseStatusCode(request); + // Returns HTTP error if that's the only info we have. + // if (!String.IsNullOrEmpty(www.error) && String.IsNullOrEmpty(www.text)) + if (!String.IsNullOrEmpty(request.error) && String.IsNullOrEmpty(request.downloadHandler.text)) + { + var errorString = string.Format("{{\"error\":\"{0}\"}}", request.error); + tcs.TrySetResult(new Tuple(statusCode, errorString)); + } + else + { + tcs.TrySetResult(new Tuple(statusCode, request.downloadHandler.text)); + } + } + else + { + // Update upload progress + var newUploadProgress = request.uploadProgress; + if (oldUploadProgress < newUploadProgress) + { + uploadProgress.Report(new AVUploadProgressEventArgs { Progress = newUploadProgress }); + } + oldUploadProgress = newUploadProgress; + + // Update download progress + var newDownloadProgress = request.downloadProgress; + if (oldDownloadProgress < newDownloadProgress) + { + downloadProgress.Report(new AVDownloadProgressEventArgs { Progress = newDownloadProgress }); + } + oldDownloadProgress = newDownloadProgress; + } + }); + }); + }); + + // Get off of the main thread for further processing. + return tcs.Task.ContinueWith(t => + { + var dispatchTcs = new TaskCompletionSource(); + // ThreadPool doesn't work well in IL2CPP environment, but Thread does! + if (isCompiledByIL2CPP) + { + var thread = new Thread(_ => + { + dispatchTcs.TrySetResult(null); + }); + thread.Start(); + } + else + { + ThreadPool.QueueUserWorkItem(_ => dispatchTcs.TrySetResult(null)); + } + return dispatchTcs.Task; + }).Unwrap() + .ContinueWith(_ => tcs.Task).Unwrap(); + } + + private static HttpStatusCode GetResponseStatusCode(UnityWebRequest request) + { + if (Enum.IsDefined(typeof(HttpStatusCode), (int)request.responseCode)) + { + return (HttpStatusCode)request.responseCode; + } + return (HttpStatusCode)400; + } + + private static UnityWebRequest GenerateRequest(HttpRequest request, byte[] bytes) + { + var webRequest = new UnityWebRequest(); + webRequest.method = request.Method; + webRequest.url = request.Uri.AbsoluteUri; + // Explicitly assume a JSON content. + webRequest.SetRequestHeader("Content-Type", "application/json"); + //webRequest.SetRequestHeader("User-Agent", "net-unity-" + AVVersionInfo.Version); + if (request.Headers != null) + { + foreach (var header in request.Headers) + { + webRequest.SetRequestHeader(header.Key as string, header.Value as string); + } + } + + if (bytes != null) + { + webRequest.uploadHandler = new UploadHandlerRaw(bytes); + } + webRequest.downloadHandler = new DownloadHandlerBuffer(); + webRequest.Send(); + return webRequest; + } + + private static void WaitForWebRequest(UnityWebRequest request, Action action) + { + Dispatcher.Instance.Post(() => + { + var isDone = request.isDone; + action(request); + if (!isDone) + { + WaitForWebRequest(request, action); + } + }); + } + } +} diff --git a/Storage/Source/Internal/IAVCorePlugins.cs b/Storage/Source/Internal/IAVCorePlugins.cs new file mode 100644 index 0000000..51060bd --- /dev/null +++ b/Storage/Source/Internal/IAVCorePlugins.cs @@ -0,0 +1,26 @@ +using LeanCloud.Storage.Internal; +using System; + +namespace LeanCloud.Storage.Internal +{ + public interface IAVCorePlugins + { + void Reset(); + + IHttpClient HttpClient { get; } + IAppRouterController AppRouterController { get; } + IAVCommandRunner CommandRunner { get; } + IStorageController StorageController { get; } + + IAVCloudCodeController CloudCodeController { get; } + IAVConfigController ConfigController { get; } + IAVFileController FileController { get; } + IAVObjectController ObjectController { get; } + IAVQueryController QueryController { get; } + IAVSessionController SessionController { get; } + IAVUserController UserController { get; } + IObjectSubclassingController SubclassingController { get; } + IAVCurrentUserController CurrentUserController { get; } + IInstallationIdController InstallationIdController { get; } + } +} \ No newline at end of file diff --git a/Storage/Source/Internal/InstallationId/Controller/IInstallationIdController.cs b/Storage/Source/Internal/InstallationId/Controller/IInstallationIdController.cs new file mode 100644 index 0000000..1fb9af4 --- /dev/null +++ b/Storage/Source/Internal/InstallationId/Controller/IInstallationIdController.cs @@ -0,0 +1,24 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace LeanCloud.Storage.Internal { + public interface IInstallationIdController { + /// + /// Sets current installationId and saves it to local storage. + /// + /// The installationId to be saved. + Task SetAsync(Guid? installationId); + + /// + /// Gets current installationId from local storage. Generates a none exists. + /// + /// Current installationId. + Task GetAsync(); + + /// + /// Clears current installationId from memory and local storage. + /// + Task ClearAsync(); + } +} diff --git a/Storage/Source/Internal/InstallationId/Controller/InstallationIdController.cs b/Storage/Source/Internal/InstallationId/Controller/InstallationIdController.cs new file mode 100644 index 0000000..d28b015 --- /dev/null +++ b/Storage/Source/Internal/InstallationId/Controller/InstallationIdController.cs @@ -0,0 +1,66 @@ +using LeanCloud.Storage.Internal; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace LeanCloud.Storage.Internal { + public class InstallationIdController : IInstallationIdController { + private const string InstallationIdKey = "InstallationId"; + private readonly object mutex = new object(); + private Guid? installationId; + + private readonly IStorageController storageController; + public InstallationIdController(IStorageController storageController) { + this.storageController = storageController; + } + + public Task SetAsync(Guid? installationId) { + lock (mutex) { + Task saveTask; + + if (installationId == null) { + saveTask = storageController + .LoadAsync() + .OnSuccess(storage => storage.Result.RemoveAsync(InstallationIdKey)) + .Unwrap(); + } else { + saveTask = storageController + .LoadAsync() + .OnSuccess(storage => storage.Result.AddAsync(InstallationIdKey, installationId.ToString())) + .Unwrap(); + } + this.installationId = installationId; + return saveTask; + } + } + + public Task GetAsync() { + lock (mutex) { + if (installationId != null) { + return Task.FromResult(installationId); + } + } + + return storageController + .LoadAsync() + .OnSuccess, Task>(s => { + object id; + s.Result.TryGetValue(InstallationIdKey, out id); + try { + lock (mutex) { + installationId = new Guid((string)id); + return Task.FromResult(installationId); + } + } catch (Exception) { + var newInstallationId = Guid.NewGuid(); + return SetAsync(newInstallationId).OnSuccess(_ => newInstallationId); + } + }) + .Unwrap(); + } + + public Task ClearAsync() { + return SetAsync(null); + } + } +} diff --git a/Storage/Source/Internal/Object/Controller/AVObjectController.cs b/Storage/Source/Internal/Object/Controller/AVObjectController.cs new file mode 100644 index 0000000..63e5a30 --- /dev/null +++ b/Storage/Source/Internal/Object/Controller/AVObjectController.cs @@ -0,0 +1,248 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using LeanCloud.Utilities; +using LeanCloud.Storage.Internal; + +namespace LeanCloud.Storage.Internal +{ + public class AVObjectController : IAVObjectController + { + private readonly IAVCommandRunner commandRunner; + + public AVObjectController(IAVCommandRunner commandRunner) + { + this.commandRunner = commandRunner; + } + + public Task FetchAsync(IObjectState state, + string sessionToken, + CancellationToken cancellationToken) + { + var command = new AVCommand(string.Format("classes/{0}/{1}", + Uri.EscapeDataString(state.ClassName), + Uri.EscapeDataString(state.ObjectId)), + method: "GET", + sessionToken: sessionToken, + data: null); + + return commandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).OnSuccess(t => + { + return AVObjectCoder.Instance.Decode(t.Result.Item2, AVDecoder.Instance); + }); + } + + public Task FetchAsync(IObjectState state, + IDictionary queryString, + string sessionToken, + CancellationToken cancellationToken) + { + var command = new AVCommand(string.Format("classes/{0}/{1}?{2}", + Uri.EscapeDataString(state.ClassName), + Uri.EscapeDataString(state.ObjectId), + AVClient.BuildQueryString(queryString)), + method: "GET", + sessionToken: sessionToken, + data: null); + + return commandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).OnSuccess(t => + { + return AVObjectCoder.Instance.Decode(t.Result.Item2, AVDecoder.Instance); + }); + } + + public Task SaveAsync(IObjectState state, + IDictionary operations, + string sessionToken, + CancellationToken cancellationToken) + { + var objectJSON = AVObject.ToJSONObjectForSaving(operations); + + var command = new AVCommand((state.ObjectId == null ? + string.Format("classes/{0}", Uri.EscapeDataString(state.ClassName)) : + string.Format("classes/{0}/{1}", Uri.EscapeDataString(state.ClassName), state.ObjectId)), + method: (state.ObjectId == null ? "POST" : "PUT"), + sessionToken: sessionToken, + data: objectJSON); + + return commandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).OnSuccess(t => + { + var serverState = AVObjectCoder.Instance.Decode(t.Result.Item2, AVDecoder.Instance); + serverState = serverState.MutatedClone(mutableClone => + { + mutableClone.IsNew = t.Result.Item1 == System.Net.HttpStatusCode.Created; + }); + return serverState; + }); + } + + public IList> SaveAllAsync(IList states, + IList> operationsList, + string sessionToken, + CancellationToken cancellationToken) + { + + var requests = states + .Zip(operationsList, (item, ops) => new AVCommand( + item.ObjectId == null + ? string.Format("classes/{0}", Uri.EscapeDataString(item.ClassName)) + : string.Format("classes/{0}/{1}", Uri.EscapeDataString(item.ClassName), Uri.EscapeDataString(item.ObjectId)), + method: item.ObjectId == null ? "POST" : "PUT", + data: AVObject.ToJSONObjectForSaving(ops))) + .ToList(); + + var batchTasks = ExecuteBatchRequests(requests, sessionToken, cancellationToken); + var stateTasks = new List>(); + foreach (var task in batchTasks) + { + stateTasks.Add(task.OnSuccess(t => + { + return AVObjectCoder.Instance.Decode(t.Result, AVDecoder.Instance); + })); + } + + return stateTasks; + } + + public Task DeleteAsync(IObjectState state, + string sessionToken, + CancellationToken cancellationToken) + { + var command = new AVCommand(string.Format("classes/{0}/{1}", + state.ClassName, state.ObjectId), + method: "DELETE", + sessionToken: sessionToken, + data: null); + + return commandRunner.RunCommandAsync(command, cancellationToken: cancellationToken); + } + + public IList DeleteAllAsync(IList states, + string sessionToken, + CancellationToken cancellationToken) + { + var requests = states + .Where(item => item.ObjectId != null) + .Select(item => new AVCommand( + string.Format("classes/{0}/{1}", Uri.EscapeDataString(item.ClassName), Uri.EscapeDataString(item.ObjectId)), + method: "DELETE", + data: null)) + .ToList(); + return ExecuteBatchRequests(requests, sessionToken, cancellationToken).Cast().ToList(); + } + + // TODO (hallucinogen): move this out to a class to be used by Analytics + private const int MaximumBatchSize = 50; + internal IList>> ExecuteBatchRequests(IList requests, + string sessionToken, + CancellationToken cancellationToken) + { + var tasks = new List>>(); + int batchSize = requests.Count; + + IEnumerable remaining = requests; + while (batchSize > MaximumBatchSize) + { + var process = remaining.Take(MaximumBatchSize).ToList(); + remaining = remaining.Skip(MaximumBatchSize); + + tasks.AddRange(ExecuteBatchRequest(process, sessionToken, cancellationToken)); + + batchSize = remaining.Count(); + } + tasks.AddRange(ExecuteBatchRequest(remaining.ToList(), sessionToken, cancellationToken)); + + return tasks; + } + + private IList>> ExecuteBatchRequest(IList requests, + string sessionToken, + CancellationToken cancellationToken) + { + var tasks = new List>>(); + int batchSize = requests.Count; + var tcss = new List>>(); + for (int i = 0; i < batchSize; ++i) + { + var tcs = new TaskCompletionSource>(); + tcss.Add(tcs); + tasks.Add(tcs.Task); + } + + var encodedRequests = requests.Select(r => + { + var results = new Dictionary { + { "method", r.Method }, + { "path", r.Uri.AbsolutePath }, + }; + + if (r.DataObject != null) + { + results["body"] = r.DataObject; + } + return results; + }).Cast().ToList(); + var command = new AVCommand("batch", + method: "POST", + sessionToken: sessionToken, + data: new Dictionary { { "requests", encodedRequests } }); + + commandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).ContinueWith(t => + { + if (t.IsFaulted || t.IsCanceled) + { + foreach (var tcs in tcss) + { + if (t.IsFaulted) + { + tcs.TrySetException(t.Exception); + } + else if (t.IsCanceled) + { + tcs.TrySetCanceled(); + } + } + return; + } + + var resultsArray = Conversion.As>(t.Result.Item2["results"]); + int resultLength = resultsArray.Count; + if (resultLength != batchSize) + { + foreach (var tcs in tcss) + { + tcs.TrySetException(new InvalidOperationException( + "Batch command result count expected: " + batchSize + " but was: " + resultLength + ".")); + } + return; + } + + for (int i = 0; i < batchSize; ++i) + { + var result = resultsArray[i] as Dictionary; + var tcs = tcss[i]; + + if (result.ContainsKey("success")) + { + tcs.TrySetResult(result["success"] as IDictionary); + } + else if (result.ContainsKey("error")) + { + var error = result["error"] as IDictionary; + long errorCode = long.Parse(error["code"].ToString()); + tcs.TrySetException(new AVException((AVException.ErrorCode)errorCode, error["error"] as string)); + } + else + { + tcs.TrySetException(new InvalidOperationException( + "Invalid batch command response.")); + } + } + }); + + return tasks; + } + } +} diff --git a/Storage/Source/Internal/Object/Controller/IAVObjectController.cs b/Storage/Source/Internal/Object/Controller/IAVObjectController.cs new file mode 100644 index 0000000..9230eb5 --- /dev/null +++ b/Storage/Source/Internal/Object/Controller/IAVObjectController.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace LeanCloud.Storage.Internal +{ + public interface IAVObjectController + { + //Task FetchAsync(IObjectState state, + // string sessionToken, + // CancellationToken cancellationToken); + + Task FetchAsync(IObjectState state, + IDictionary queryString, + string sessionToken, + CancellationToken cancellationToken); + + Task SaveAsync(IObjectState state, + IDictionary operations, + string sessionToken, + CancellationToken cancellationToken); + + IList> SaveAllAsync(IList states, + IList> operationsList, + string sessionToken, + CancellationToken cancellationToken); + + Task DeleteAsync(IObjectState state, + string sessionToken, + CancellationToken cancellationToken); + + IList DeleteAllAsync(IList states, + string sessionToken, + CancellationToken cancellationToken); + } +} diff --git a/Storage/Source/Internal/Object/Controller/IAVObjectCurrentController.cs b/Storage/Source/Internal/Object/Controller/IAVObjectCurrentController.cs new file mode 100644 index 0000000..e9b6f40 --- /dev/null +++ b/Storage/Source/Internal/Object/Controller/IAVObjectCurrentController.cs @@ -0,0 +1,51 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace LeanCloud.Storage.Internal { + /// + /// IAVObjectCurrentController controls the single-instance + /// persistence used throughout the code-base. Sample usages are and + /// . + /// + /// Type of object being persisted. + public interface IAVObjectCurrentController where T : AVObject { + /// + /// Persists current . + /// + /// to be persisted. + /// The cancellation token. + Task SetAsync(T obj, CancellationToken cancellationToken); + + /// + /// Gets the persisted current . + /// + /// The cancellation token. + Task GetAsync(CancellationToken cancellationToken); + + /// + /// Returns a that resolves to true if current + /// exists. + /// + /// The cancellation token. + Task ExistsAsync(CancellationToken cancellationToken); + + /// + /// Returns true if the given is the persisted current + /// . + /// + /// The object to check. + /// True if obj is the current persisted . + bool IsCurrent(T obj); + + /// + /// Nullifies the current from memory. + /// + void ClearFromMemory(); + + /// + /// Clears current from disk. + /// + void ClearFromDisk(); + } +} diff --git a/Storage/Source/Internal/Object/State/IObjectState.cs b/Storage/Source/Internal/Object/State/IObjectState.cs new file mode 100644 index 0000000..ab7b074 --- /dev/null +++ b/Storage/Source/Internal/Object/State/IObjectState.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; + +namespace LeanCloud.Storage.Internal +{ + public interface IObjectState : IEnumerable> + { + bool IsNew { get; } + string ClassName { get; } + string ObjectId { get; } + DateTime? UpdatedAt { get; } + DateTime? CreatedAt { get; } + object this[string key] { get; } + + bool ContainsKey(string key); + + IObjectState MutatedClone(Action func); + } +} diff --git a/Storage/Source/Internal/Object/State/MutableObjectState.cs b/Storage/Source/Internal/Object/State/MutableObjectState.cs new file mode 100644 index 0000000..ff9be3a --- /dev/null +++ b/Storage/Source/Internal/Object/State/MutableObjectState.cs @@ -0,0 +1,114 @@ +using System; +using System.Linq; +using System.Collections.Generic; + +namespace LeanCloud.Storage.Internal +{ + public class MutableObjectState : IObjectState + { + public bool IsNew { get; set; } + public string ClassName { get; set; } + public string ObjectId { get; set; } + public DateTime? UpdatedAt { get; set; } + public DateTime? CreatedAt { get; set; } + + // Initialize serverData to avoid further null checking. + private IDictionary serverData = new Dictionary(); + public IDictionary ServerData + { + get + { + return serverData; + } + + set + { + serverData = value; + } + } + + public object this[string key] + { + get + { + return ServerData[key]; + } + } + + public bool ContainsKey(string key) + { + return ServerData.ContainsKey(key); + } + + public void Apply(IDictionary operationSet) + { + // Apply operationSet + foreach (var pair in operationSet) + { + object oldValue; + ServerData.TryGetValue(pair.Key, out oldValue); + var newValue = pair.Value.Apply(oldValue, pair.Key); + if (newValue != AVDeleteOperation.DeleteToken) + { + ServerData[pair.Key] = newValue; + } + else + { + ServerData.Remove(pair.Key); + } + } + } + + public void Apply(IObjectState other) + { + IsNew = other.IsNew; + if (other.ObjectId != null) + { + ObjectId = other.ObjectId; + } + if (other.UpdatedAt != null) + { + UpdatedAt = other.UpdatedAt; + } + if (other.CreatedAt != null) + { + CreatedAt = other.CreatedAt; + } + + foreach (var pair in other) + { + ServerData[pair.Key] = pair.Value; + } + } + + public IObjectState MutatedClone(Action func) + { + var clone = MutableClone(); + func(clone); + return clone; + } + + protected virtual MutableObjectState MutableClone() + { + return new MutableObjectState + { + IsNew = IsNew, + ClassName = ClassName, + ObjectId = ObjectId, + CreatedAt = CreatedAt, + UpdatedAt = UpdatedAt, + ServerData = this.ToDictionary(t => t.Key, t => t.Value) + }; + } + + IEnumerator> IEnumerable>.GetEnumerator() + { + return ServerData.GetEnumerator(); + } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return ((IEnumerable>)this).GetEnumerator(); + } + } +} diff --git a/Storage/Source/Internal/Object/Subclassing/IObjectSubclassingController.cs b/Storage/Source/Internal/Object/Subclassing/IObjectSubclassingController.cs new file mode 100644 index 0000000..38322fc --- /dev/null +++ b/Storage/Source/Internal/Object/Subclassing/IObjectSubclassingController.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LeanCloud.Storage.Internal +{ + public interface IObjectSubclassingController + { + String GetClassName(Type type); + Type GetType(String className); + + bool IsTypeValid(String className, Type type); + + void RegisterSubclass(Type t); + void UnregisterSubclass(Type t); + + void AddRegisterHook(Type t, Action action); + + AVObject Instantiate(String className); + IDictionary GetPropertyMappings(String className); + } +} diff --git a/Storage/Source/Internal/Object/Subclassing/ObjectSubclassInfo.cs b/Storage/Source/Internal/Object/Subclassing/ObjectSubclassInfo.cs new file mode 100644 index 0000000..080fe6c --- /dev/null +++ b/Storage/Source/Internal/Object/Subclassing/ObjectSubclassInfo.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Reflection; +using LeanCloud.Storage.Internal; + +namespace LeanCloud.Storage.Internal +{ + internal class ObjectSubclassInfo + { + public ObjectSubclassInfo(Type type, ConstructorInfo constructor) + { + TypeInfo = type.GetTypeInfo(); + ClassName = GetClassName(TypeInfo); + Constructor = constructor; + PropertyMappings = ReflectionHelpers.GetProperties(type) + .Select(prop => Tuple.Create(prop, prop.GetCustomAttribute(true))) + .Where(t => t.Item2 != null) + .Select(t => Tuple.Create(t.Item1, t.Item2.FieldName)) + .ToDictionary(t => t.Item1.Name, t => t.Item2); + } + + public TypeInfo TypeInfo { get; private set; } + public String ClassName { get; private set; } + public IDictionary PropertyMappings { get; private set; } + private ConstructorInfo Constructor { get; set; } + + public AVObject Instantiate() + { + return (AVObject)Constructor.Invoke(null); + } + + internal static String GetClassName(TypeInfo type) + { + var attribute = type.GetCustomAttribute(); + return attribute != null ? attribute.ClassName : null; + } + } +} diff --git a/Storage/Source/Internal/Object/Subclassing/ObjectSubclassingController.cs b/Storage/Source/Internal/Object/Subclassing/ObjectSubclassingController.cs new file mode 100644 index 0000000..b1f913e --- /dev/null +++ b/Storage/Source/Internal/Object/Subclassing/ObjectSubclassingController.cs @@ -0,0 +1,171 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading; +using LeanCloud.Storage.Internal; + +namespace LeanCloud.Storage.Internal +{ + internal class ObjectSubclassingController : IObjectSubclassingController + { + // Class names starting with _ are documented to be reserved. Use this one + // here to allow us to 'inherit' certain properties. + private static readonly string avObjectClassName = "_AVObject"; + + private readonly ReaderWriterLockSlim mutex; + private readonly IDictionary registeredSubclasses; + private Dictionary registerActions; + + public ObjectSubclassingController() + { + mutex = new ReaderWriterLockSlim(); + registeredSubclasses = new Dictionary(); + registerActions = new Dictionary(); + + // Register the AVObject subclass, so we get access to the ACL, + // objectId, and other AVFieldName properties. + RegisterSubclass(typeof(AVObject)); + } + + public String GetClassName(Type type) + { + return type == typeof(AVObject) + ? avObjectClassName + : ObjectSubclassInfo.GetClassName(type.GetTypeInfo()); + } + + public Type GetType(String className) + { + ObjectSubclassInfo info = null; + mutex.EnterReadLock(); + registeredSubclasses.TryGetValue(className, out info); + mutex.ExitReadLock(); + + return info != null + ? info.TypeInfo.AsType() + : null; + } + + public bool IsTypeValid(String className, Type type) + { + ObjectSubclassInfo subclassInfo = null; + + mutex.EnterReadLock(); + registeredSubclasses.TryGetValue(className, out subclassInfo); + mutex.ExitReadLock(); + + return subclassInfo == null + ? type == typeof(AVObject) + : subclassInfo.TypeInfo == type.GetTypeInfo(); + } + + public void RegisterSubclass(Type type) + { + TypeInfo typeInfo = type.GetTypeInfo(); + if (!typeof(AVObject).GetTypeInfo().IsAssignableFrom(typeInfo)) + { + throw new ArgumentException("Cannot register a type that is not a subclass of AVObject"); + } + + String className = GetClassName(type); + + try + { + // Perform this as a single independent transaction, so we can never get into an + // intermediate state where we *theoretically* register the wrong class due to a + // TOCTTOU bug. + mutex.EnterWriteLock(); + + ObjectSubclassInfo previousInfo = null; + if (registeredSubclasses.TryGetValue(className, out previousInfo)) + { + if (typeInfo.IsAssignableFrom(previousInfo.TypeInfo)) + { + // Previous subclass is more specific or equal to the current type, do nothing. + return; + } + else if (previousInfo.TypeInfo.IsAssignableFrom(typeInfo)) + { + // Previous subclass is parent of new child, fallthrough and actually register + // this class. + /* Do nothing */ + } + else + { + throw new ArgumentException( + "Tried to register both " + previousInfo.TypeInfo.FullName + " and " + typeInfo.FullName + + " as the AVObject subclass of " + className + ". Cannot determine the right class " + + "to use because neither inherits from the other." + ); + } + } + + ConstructorInfo constructor = type.FindConstructor(); + if (constructor == null) + { + throw new ArgumentException("Cannot register a type that does not implement the default constructor!"); + } + + registeredSubclasses[className] = new ObjectSubclassInfo(type, constructor); + } + finally + { + mutex.ExitWriteLock(); + } + + Action toPerform; + + mutex.EnterReadLock(); + registerActions.TryGetValue(className, out toPerform); + mutex.ExitReadLock(); + + if (toPerform != null) + { + toPerform(); + } + } + + public void UnregisterSubclass(Type type) + { + mutex.EnterWriteLock(); + registeredSubclasses.Remove(GetClassName(type)); + mutex.ExitWriteLock(); + } + + public void AddRegisterHook(Type t, Action action) + { + mutex.EnterWriteLock(); + registerActions.Add(GetClassName(t), action); + mutex.ExitWriteLock(); + } + + public AVObject Instantiate(String className) + { + ObjectSubclassInfo info = null; + + mutex.EnterReadLock(); + registeredSubclasses.TryGetValue(className, out info); + mutex.ExitReadLock(); + + return info != null + ? info.Instantiate() + : new AVObject(className); + } + + public IDictionary GetPropertyMappings(String className) + { + ObjectSubclassInfo info = null; + mutex.EnterReadLock(); + registeredSubclasses.TryGetValue(className, out info); + if (info == null) + { + registeredSubclasses.TryGetValue(avObjectClassName, out info); + } + mutex.ExitReadLock(); + + return info.PropertyMappings; + } + + } +} diff --git a/Storage/Source/Internal/Operation/AVAddOperation.cs b/Storage/Source/Internal/Operation/AVAddOperation.cs new file mode 100644 index 0000000..ed0a83e --- /dev/null +++ b/Storage/Source/Internal/Operation/AVAddOperation.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using LeanCloud.Utilities; + +namespace LeanCloud.Storage.Internal { + public class AVAddOperation : IAVFieldOperation { + private ReadOnlyCollection objects; + public AVAddOperation(IEnumerable objects) { + this.objects = new ReadOnlyCollection(objects.ToList()); + } + + public object Encode() { + return new Dictionary { + {"__op", "Add"}, + {"objects", PointerOrLocalIdEncoder.Instance.Encode(objects)} + }; + } + + public IAVFieldOperation MergeWithPrevious(IAVFieldOperation previous) { + if (previous == null) { + return this; + } + if (previous is AVDeleteOperation) { + return new AVSetOperation(objects.ToList()); + } + if (previous is AVSetOperation) { + var setOp = (AVSetOperation)previous; + var oldList = Conversion.To>(setOp.Value); + return new AVSetOperation(oldList.Concat(objects).ToList()); + } + if (previous is AVAddOperation) { + return new AVAddOperation(((AVAddOperation)previous).Objects.Concat(objects)); + } + throw new InvalidOperationException("Operation is invalid after previous operation."); + } + + public object Apply(object oldValue, string key) { + if (oldValue == null) { + return objects.ToList(); + } + var oldList = Conversion.To>(oldValue); + return oldList.Concat(objects).ToList(); + } + + public IEnumerable Objects { + get { + return objects; + } + } + } +} diff --git a/Storage/Source/Internal/Operation/AVAddUniqueOperation.cs b/Storage/Source/Internal/Operation/AVAddUniqueOperation.cs new file mode 100644 index 0000000..8e0a957 --- /dev/null +++ b/Storage/Source/Internal/Operation/AVAddUniqueOperation.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using LeanCloud.Utilities; + +namespace LeanCloud.Storage.Internal { + public class AVAddUniqueOperation : IAVFieldOperation { + private ReadOnlyCollection objects; + public AVAddUniqueOperation(IEnumerable objects) { + this.objects = new ReadOnlyCollection(objects.Distinct().ToList()); + } + + public object Encode() { + return new Dictionary { + {"__op", "AddUnique"}, + {"objects", PointerOrLocalIdEncoder.Instance.Encode(objects)} + }; + } + + public IAVFieldOperation MergeWithPrevious(IAVFieldOperation previous) { + if (previous == null) { + return this; + } + if (previous is AVDeleteOperation) { + return new AVSetOperation(objects.ToList()); + } + if (previous is AVSetOperation) { + var setOp = (AVSetOperation)previous; + var oldList = Conversion.To>(setOp.Value); + var result = this.Apply(oldList, null); + return new AVSetOperation(result); + } + if (previous is AVAddUniqueOperation) { + var oldList = ((AVAddUniqueOperation)previous).Objects; + return new AVAddUniqueOperation((IList)this.Apply(oldList, null)); + } + throw new InvalidOperationException("Operation is invalid after previous operation."); + } + + public object Apply(object oldValue, string key) { + if (oldValue == null) { + return objects.ToList(); + } + var newList = Conversion.To>(oldValue).ToList(); + var comparer = AVFieldOperations.AVObjectComparer; + foreach (var objToAdd in objects) { + if (objToAdd is AVObject) { + var matchedObj = newList.FirstOrDefault(listObj => comparer.Equals(objToAdd, listObj)); + if (matchedObj == null) { + newList.Add(objToAdd); + } else { + var index = newList.IndexOf(matchedObj); + newList[index] = objToAdd; + } + } else if (!newList.Contains(objToAdd, comparer)) { + newList.Add(objToAdd); + } + } + return newList; + } + + public IEnumerable Objects { + get { + return objects; + } + } + } +} diff --git a/Storage/Source/Internal/Operation/AVDeleteOperation.cs b/Storage/Source/Internal/Operation/AVDeleteOperation.cs new file mode 100644 index 0000000..7b77b94 --- /dev/null +++ b/Storage/Source/Internal/Operation/AVDeleteOperation.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; + +namespace LeanCloud.Storage.Internal +{ + /// + /// An operation where a field is deleted from the object. + /// + public class AVDeleteOperation : IAVFieldOperation + { + internal static readonly object DeleteToken = new object(); + private static AVDeleteOperation _Instance = new AVDeleteOperation(); + public static AVDeleteOperation Instance + { + get + { + return _Instance; + } + } + + private AVDeleteOperation() { } + public object Encode() + { + return new Dictionary { + {"__op", "Delete"} + }; + } + + public IAVFieldOperation MergeWithPrevious(IAVFieldOperation previous) + { + return this; + } + + public object Apply(object oldValue, string key) + { + return DeleteToken; + } + } +} diff --git a/Storage/Source/Internal/Operation/AVFieldOperations.cs b/Storage/Source/Internal/Operation/AVFieldOperations.cs new file mode 100644 index 0000000..9c07ffa --- /dev/null +++ b/Storage/Source/Internal/Operation/AVFieldOperations.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; + +namespace LeanCloud.Storage.Internal { + public class AVObjectIdComparer : IEqualityComparer { + bool IEqualityComparer.Equals(object p1, object p2) { + var avObj1 = p1 as AVObject; + var avObj2 = p2 as AVObject; + if (avObj1 != null && avObj2 != null) { + return object.Equals(avObj1.ObjectId, avObj2.ObjectId); + } + return object.Equals(p1, p2); + } + + public int GetHashCode(object p) { + var avObject = p as AVObject; + if (avObject != null) { + return avObject.ObjectId.GetHashCode(); + } + return p.GetHashCode(); + } + } + + static class AVFieldOperations { + private static AVObjectIdComparer comparer; + + public static IAVFieldOperation Decode(IDictionary json) { + throw new NotImplementedException(); + } + + public static IEqualityComparer AVObjectComparer { + get { + if (comparer == null) { + comparer = new AVObjectIdComparer(); + } + return comparer; + } + } + } +} diff --git a/Storage/Source/Internal/Operation/AVIncrementOperation.cs b/Storage/Source/Internal/Operation/AVIncrementOperation.cs new file mode 100644 index 0000000..1606e5f --- /dev/null +++ b/Storage/Source/Internal/Operation/AVIncrementOperation.cs @@ -0,0 +1,166 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace LeanCloud.Storage.Internal +{ + public class AVIncrementOperation : IAVFieldOperation + { + private static readonly IDictionary, Func> adders; + + static AVIncrementOperation() + { + // Defines adders for all of the implicit conversions: http://msdn.microsoft.com/en-US/library/y5b434w4(v=vs.80).aspx + adders = new Dictionary, Func> { + {new Tuple(typeof(sbyte), typeof(sbyte)), (left, right) => (sbyte)left + (sbyte)right}, + {new Tuple(typeof(sbyte), typeof(short)), (left, right) => (sbyte)left + (short)right}, + {new Tuple(typeof(sbyte), typeof(int)), (left, right) => (sbyte)left + (int)right}, + {new Tuple(typeof(sbyte), typeof(long)), (left, right) => (sbyte)left + (long)right}, + {new Tuple(typeof(sbyte), typeof(float)), (left, right) => (sbyte)left + (float)right}, + {new Tuple(typeof(sbyte), typeof(double)), (left, right) => (sbyte)left + (double)right}, + {new Tuple(typeof(sbyte), typeof(decimal)), (left, right) => (sbyte)left + (decimal)right}, + {new Tuple(typeof(byte), typeof(byte)), (left, right) => (byte)left + (byte)right}, + {new Tuple(typeof(byte), typeof(short)), (left, right) => (byte)left + (short)right}, + {new Tuple(typeof(byte), typeof(ushort)), (left, right) => (byte)left + (ushort)right}, + {new Tuple(typeof(byte), typeof(int)), (left, right) => (byte)left + (int)right}, + {new Tuple(typeof(byte), typeof(uint)), (left, right) => (byte)left + (uint)right}, + {new Tuple(typeof(byte), typeof(long)), (left, right) => (byte)left + (long)right}, + {new Tuple(typeof(byte), typeof(ulong)), (left, right) => (byte)left + (ulong)right}, + {new Tuple(typeof(byte), typeof(float)), (left, right) => (byte)left + (float)right}, + {new Tuple(typeof(byte), typeof(double)), (left, right) => (byte)left + (double)right}, + {new Tuple(typeof(byte), typeof(decimal)), (left, right) => (byte)left + (decimal)right}, + {new Tuple(typeof(short), typeof(short)), (left, right) => (short)left + (short)right}, + {new Tuple(typeof(short), typeof(int)), (left, right) => (short)left + (int)right}, + {new Tuple(typeof(short), typeof(long)), (left, right) => (short)left + (long)right}, + {new Tuple(typeof(short), typeof(float)), (left, right) => (short)left + (float)right}, + {new Tuple(typeof(short), typeof(double)), (left, right) => (short)left + (double)right}, + {new Tuple(typeof(short), typeof(decimal)), (left, right) => (short)left + (decimal)right}, + {new Tuple(typeof(ushort), typeof(ushort)), (left, right) => (ushort)left + (ushort)right}, + {new Tuple(typeof(ushort), typeof(int)), (left, right) => (ushort)left + (int)right}, + {new Tuple(typeof(ushort), typeof(uint)), (left, right) => (ushort)left + (uint)right}, + {new Tuple(typeof(ushort), typeof(long)), (left, right) => (ushort)left + (long)right}, + {new Tuple(typeof(ushort), typeof(ulong)), (left, right) => (ushort)left + (ulong)right}, + {new Tuple(typeof(ushort), typeof(float)), (left, right) => (ushort)left + (float)right}, + {new Tuple(typeof(ushort), typeof(double)), (left, right) => (ushort)left + (double)right}, + {new Tuple(typeof(ushort), typeof(decimal)), (left, right) => (ushort)left + (decimal)right}, + {new Tuple(typeof(int), typeof(int)), (left, right) => (int)left + (int)right}, + {new Tuple(typeof(int), typeof(long)), (left, right) => (int)left + (long)right}, + {new Tuple(typeof(int), typeof(float)), (left, right) => (int)left + (float)right}, + {new Tuple(typeof(int), typeof(double)), (left, right) => (int)left + (double)right}, + {new Tuple(typeof(int), typeof(decimal)), (left, right) => (int)left + (decimal)right}, + {new Tuple(typeof(uint), typeof(uint)), (left, right) => (uint)left + (uint)right}, + {new Tuple(typeof(uint), typeof(long)), (left, right) => (uint)left + (long)right}, + {new Tuple(typeof(uint), typeof(ulong)), (left, right) => (uint)left + (ulong)right}, + {new Tuple(typeof(uint), typeof(float)), (left, right) => (uint)left + (float)right}, + {new Tuple(typeof(uint), typeof(double)), (left, right) => (uint)left + (double)right}, + {new Tuple(typeof(uint), typeof(decimal)), (left, right) => (uint)left + (decimal)right}, + {new Tuple(typeof(long), typeof(long)), (left, right) => (long)left + (long)right}, + {new Tuple(typeof(long), typeof(float)), (left, right) => (long)left + (float)right}, + {new Tuple(typeof(long), typeof(double)), (left, right) => (long)left + (double)right}, + {new Tuple(typeof(long), typeof(decimal)), (left, right) => (long)left + (decimal)right}, + {new Tuple(typeof(char), typeof(char)), (left, right) => (char)left + (char)right}, + {new Tuple(typeof(char), typeof(ushort)), (left, right) => (char)left + (ushort)right}, + {new Tuple(typeof(char), typeof(int)), (left, right) => (char)left + (int)right}, + {new Tuple(typeof(char), typeof(uint)), (left, right) => (char)left + (uint)right}, + {new Tuple(typeof(char), typeof(long)), (left, right) => (char)left + (long)right}, + {new Tuple(typeof(char), typeof(ulong)), (left, right) => (char)left + (ulong)right}, + {new Tuple(typeof(char), typeof(float)), (left, right) => (char)left + (float)right}, + {new Tuple(typeof(char), typeof(double)), (left, right) => (char)left + (double)right}, + {new Tuple(typeof(char), typeof(decimal)), (left, right) => (char)left + (decimal)right}, + {new Tuple(typeof(float), typeof(float)), (left, right) => (float)left + (float)right}, + {new Tuple(typeof(float), typeof(double)), (left, right) => (float)left + (double)right}, + {new Tuple(typeof(ulong), typeof(ulong)), (left, right) => (ulong)left + (ulong)right}, + {new Tuple(typeof(ulong), typeof(float)), (left, right) => (ulong)left + (float)right}, + {new Tuple(typeof(ulong), typeof(double)), (left, right) => (ulong)left + (double)right}, + {new Tuple(typeof(ulong), typeof(decimal)), (left, right) => (ulong)left + (decimal)right}, + {new Tuple(typeof(double), typeof(double)), (left, right) => (double)left + (double)right}, + {new Tuple(typeof(decimal), typeof(decimal)), (left, right) => (decimal)left + (decimal)right} + }; + // Generate the adders in the other direction + foreach (var pair in adders.Keys.ToList()) + { + if (pair.Item1.Equals(pair.Item2)) + { + continue; + } + var reversePair = new Tuple(pair.Item2, pair.Item1); + var func = adders[pair]; + adders[reversePair] = (left, right) => func(right, left); + } + } + + private object amount; + + public AVIncrementOperation(object amount) + { + this.amount = amount; + } + + public object Encode() + { + return new Dictionary + { + {"__op", "Increment"}, + {"amount", amount} + }; + } + + private static object Add(object obj1, object obj2) + { + Func adder; + if (adders.TryGetValue(new Tuple(obj1.GetType(), obj2.GetType()), out adder)) + { + return adder(obj1, obj2); + } + throw new InvalidCastException("Cannot add " + obj1.GetType() + " to " + obj2.GetType()); + } + + public IAVFieldOperation MergeWithPrevious(IAVFieldOperation previous) + { + if (previous == null) + { + return this; + } + if (previous is AVDeleteOperation) + { + return new AVSetOperation(amount); + } + if (previous is AVSetOperation) + { + var otherAmount = ((AVSetOperation)previous).Value; + if (otherAmount is string) + { + throw new InvalidOperationException("Cannot increment a non-number type."); + } + var myAmount = amount; + return new AVSetOperation(Add(otherAmount, myAmount)); + } + if (previous is AVIncrementOperation) + { + object otherAmount = ((AVIncrementOperation)previous).Amount; + object myAmount = amount; + return new AVIncrementOperation(Add(otherAmount, myAmount)); + } + throw new InvalidOperationException("Operation is invalid after previous operation."); + } + + public object Apply(object oldValue, string key) + { + if (oldValue is string) + { + throw new InvalidOperationException("Cannot increment a non-number type."); + } + object otherAmount = oldValue ?? 0; + object myAmount = amount; + return Add(otherAmount, myAmount); + } + + public object Amount + { + get + { + return amount; + } + } + } +} diff --git a/Storage/Source/Internal/Operation/AVRelationOperation.cs b/Storage/Source/Internal/Operation/AVRelationOperation.cs new file mode 100644 index 0000000..512b5f5 --- /dev/null +++ b/Storage/Source/Internal/Operation/AVRelationOperation.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LeanCloud.Storage.Internal { + public class AVRelationOperation : IAVFieldOperation { + private readonly IList adds; + private readonly IList removes; + private readonly string targetClassName; + + private AVRelationOperation(IEnumerable adds, + IEnumerable removes, + string targetClassName) { + this.targetClassName = targetClassName; + this.adds = new ReadOnlyCollection(adds.ToList()); + this.removes = new ReadOnlyCollection(removes.ToList()); + } + + public AVRelationOperation(IEnumerable adds, + IEnumerable removes) { + adds = adds ?? new AVObject[0]; + removes = removes ?? new AVObject[0]; + this.targetClassName = adds.Concat(removes).Select(o => o.ClassName).FirstOrDefault(); + this.adds = new ReadOnlyCollection(IdsFromObjects(adds).ToList()); + this.removes = new ReadOnlyCollection(IdsFromObjects(removes).ToList()); + } + + public object Encode() { + var adds = this.adds + .Select(id => PointerOrLocalIdEncoder.Instance.Encode( + AVObject.CreateWithoutData(targetClassName, id))) + .ToList(); + var removes = this.removes + .Select(id => PointerOrLocalIdEncoder.Instance.Encode( + AVObject.CreateWithoutData(targetClassName, id))) + .ToList(); + var addDict = adds.Count == 0 ? null : new Dictionary { + {"__op", "AddRelation"}, + {"objects", adds} + }; + var removeDict = removes.Count == 0 ? null : new Dictionary { + {"__op", "RemoveRelation"}, + {"objects", removes} + }; + + if (addDict != null && removeDict != null) { + return new Dictionary { + {"__op", "Batch"}, + {"ops", new[] {addDict, removeDict}} + }; + } + return addDict ?? removeDict; + } + + public IAVFieldOperation MergeWithPrevious(IAVFieldOperation previous) { + if (previous == null) { + return this; + } + if (previous is AVDeleteOperation) { + throw new InvalidOperationException("You can't modify a relation after deleting it."); + } + var other = previous as AVRelationOperation; + if (other != null) { + if (other.TargetClassName != TargetClassName) { + throw new InvalidOperationException( + string.Format("Related object must be of class {0}, but {1} was passed in.", + other.TargetClassName, + TargetClassName)); + } + var newAdd = adds.Union(other.adds.Except(removes)).ToList(); + var newRemove = removes.Union(other.removes.Except(adds)).ToList(); + return new AVRelationOperation(newAdd, newRemove, TargetClassName); + } + throw new InvalidOperationException("Operation is invalid after previous operation."); + } + + public object Apply(object oldValue, string key) { + if (adds.Count == 0 && removes.Count == 0) { + return null; + } + if (oldValue == null) { + return AVRelationBase.CreateRelation(null, key, targetClassName); + } + if (oldValue is AVRelationBase) { + var oldRelation = (AVRelationBase)oldValue; + var oldClassName = oldRelation.TargetClassName; + if (oldClassName != null && oldClassName != targetClassName) { + throw new InvalidOperationException("Related object must be a " + oldClassName + + ", but a " + targetClassName + " was passed in."); + } + oldRelation.TargetClassName = targetClassName; + return oldRelation; + } + throw new InvalidOperationException("Operation is invalid after previous operation."); + } + + public string TargetClassName { get { return targetClassName; } } + + private IEnumerable IdsFromObjects(IEnumerable objects) { + foreach (var obj in objects) { + if (obj.ObjectId == null) { + throw new ArgumentException( + "You can't add an unsaved AVObject to a relation."); + } + if (obj.ClassName != targetClassName) { + throw new ArgumentException(string.Format( + "Tried to create a AVRelation with 2 different types: {0} and {1}", + targetClassName, + obj.ClassName)); + } + } + return objects.Select(o => o.ObjectId).Distinct(); + } + } +} diff --git a/Storage/Source/Internal/Operation/AVRemoveOperation.cs b/Storage/Source/Internal/Operation/AVRemoveOperation.cs new file mode 100644 index 0000000..c9d14a4 --- /dev/null +++ b/Storage/Source/Internal/Operation/AVRemoveOperation.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; + +using LeanCloud.Utilities; + +namespace LeanCloud.Storage.Internal +{ + public class AVRemoveOperation : IAVFieldOperation + { + private ReadOnlyCollection objects; + public AVRemoveOperation(IEnumerable objects) + { + this.objects = new ReadOnlyCollection(objects.Distinct().ToList()); + } + + public object Encode() + { + return new Dictionary { + {"__op", "Remove"}, + {"objects", PointerOrLocalIdEncoder.Instance.Encode(objects)} + }; + } + + public IAVFieldOperation MergeWithPrevious(IAVFieldOperation previous) + { + if (previous == null) + { + return this; + } + if (previous is AVDeleteOperation) + { + return previous; + } + if (previous is AVSetOperation) + { + var setOp = (AVSetOperation)previous; + var oldList = Conversion.As>(setOp.Value); + return new AVSetOperation(this.Apply(oldList, null)); + } + if (previous is AVRemoveOperation) + { + var oldOp = (AVRemoveOperation)previous; + return new AVRemoveOperation(oldOp.Objects.Concat(objects)); + } + throw new InvalidOperationException("Operation is invalid after previous operation."); + } + + public object Apply(object oldValue, string key) + { + if (oldValue == null) + { + return new List(); + } + var oldList = Conversion.As>(oldValue); + return oldList.Except(objects, AVFieldOperations.AVObjectComparer).ToList(); + } + + public IEnumerable Objects + { + get + { + return objects; + } + } + } +} diff --git a/Storage/Source/Internal/Operation/AVSetOperation.cs b/Storage/Source/Internal/Operation/AVSetOperation.cs new file mode 100644 index 0000000..1c3a412 --- /dev/null +++ b/Storage/Source/Internal/Operation/AVSetOperation.cs @@ -0,0 +1,21 @@ +namespace LeanCloud.Storage.Internal { + public class AVSetOperation : IAVFieldOperation { + public AVSetOperation(object value) { + Value = value; + } + + public object Encode() { + return PointerOrLocalIdEncoder.Instance.Encode(Value); + } + + public IAVFieldOperation MergeWithPrevious(IAVFieldOperation previous) { + return this; + } + + public object Apply(object oldValue, string key) { + return Value; + } + + public object Value { get; private set; } + } +} diff --git a/Storage/Source/Internal/Operation/IAVFieldOperation.cs b/Storage/Source/Internal/Operation/IAVFieldOperation.cs new file mode 100644 index 0000000..70b2930 --- /dev/null +++ b/Storage/Source/Internal/Operation/IAVFieldOperation.cs @@ -0,0 +1,42 @@ +namespace LeanCloud.Storage.Internal { + /// + /// A AVFieldOperation represents a modification to a value in a AVObject. + /// For example, setting, deleting, or incrementing a value are all different kinds of + /// AVFieldOperations. AVFieldOperations themselves can be considered to be + /// immutable. + /// + public interface IAVFieldOperation { + /// + /// Converts the AVFieldOperation to a data structure that can be converted to JSON and sent to + /// LeanCloud as part of a save operation. + /// + /// An object to be JSONified. + object Encode(); + + /// + /// Returns a field operation that is composed of a previous operation followed by + /// this operation. This will not mutate either operation. However, it may return + /// this if the current operation is not affected by previous changes. + /// For example: + /// {increment by 2}.MergeWithPrevious({set to 5}) -> {set to 7} + /// {set to 5}.MergeWithPrevious({increment by 2}) -> {set to 5} + /// {add "foo"}.MergeWithPrevious({delete}) -> {set to ["foo"]} + /// {delete}.MergeWithPrevious({add "foo"}) -> {delete} /// + /// The most recent operation on the field, or null if none. + /// A new AVFieldOperation or this. + IAVFieldOperation MergeWithPrevious(IAVFieldOperation previous); + + /// + /// Returns a new estimated value based on a previous value and this operation. This + /// value is not intended to be sent to LeanCloud, but it is used locally on the client to + /// inspect the most likely current value for a field. + /// + /// The key and object are used solely for AVRelation to be able to construct objects + /// that refer back to their parents. + /// + /// The previous value for the field. + /// The key that this value is for. + /// The new value for the field. + object Apply(object oldValue, string key); + } +} diff --git a/Storage/Source/Internal/Query/Controller/AVQueryController.cs b/Storage/Source/Internal/Query/Controller/AVQueryController.cs new file mode 100644 index 0000000..42c1869 --- /dev/null +++ b/Storage/Source/Internal/Query/Controller/AVQueryController.cs @@ -0,0 +1,110 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Threading; +using System.Threading.Tasks; +using LeanCloud.Storage.Internal; + +namespace LeanCloud.Storage.Internal +{ + internal class AVQueryController : IAVQueryController + { + private readonly IAVCommandRunner commandRunner; + + public AVQueryController(IAVCommandRunner commandRunner) + { + this.commandRunner = commandRunner; + } + + public Task> FindAsync(AVQuery query, + AVUser user, + CancellationToken cancellationToken) where T : AVObject + { + string sessionToken = user != null ? user.SessionToken : null; + + return FindAsync(query.RelativeUri, query.BuildParameters(), sessionToken, cancellationToken).OnSuccess(t => + { + var items = t.Result["results"] as IList; + + return (from item in items + select AVObjectCoder.Instance.Decode(item as IDictionary, AVDecoder.Instance)); + }); + } + + public Task CountAsync(AVQuery query, + AVUser user, + CancellationToken cancellationToken) where T : AVObject + { + string sessionToken = user != null ? user.SessionToken : null; + var parameters = query.BuildParameters(); + parameters["limit"] = 0; + parameters["count"] = 1; + + return FindAsync(query.RelativeUri, parameters, sessionToken, cancellationToken).OnSuccess(t => + { + return Convert.ToInt32(t.Result["count"]); + }); + } + + public Task FirstAsync(AVQuery query, + AVUser user, + CancellationToken cancellationToken) where T : AVObject + { + string sessionToken = user != null ? user.SessionToken : null; + var parameters = query.BuildParameters(); + parameters["limit"] = 1; + + return FindAsync(query.RelativeUri, parameters, sessionToken, cancellationToken).OnSuccess(t => + { + var items = t.Result["results"] as IList; + var item = items.FirstOrDefault() as IDictionary; + + // Not found. Return empty state. + if (item == null) + { + return (IObjectState)null; + } + + return AVObjectCoder.Instance.Decode(item, AVDecoder.Instance); + }); + } + + private Task> FindAsync(string relativeUri, + IDictionary parameters, + string sessionToken, + CancellationToken cancellationToken) + { + + var command = new AVCommand(string.Format("{0}?{1}", + relativeUri, + AVClient.BuildQueryString(parameters)), + method: "GET", + sessionToken: sessionToken, + data: null); + + return commandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).OnSuccess(t => + { + return t.Result.Item2; + }); + } + + //private Task> FindAsync(string className, + // IDictionary parameters, + // string sessionToken, + // CancellationToken cancellationToken) + //{ + // var command = new AVCommand(string.Format("classes/{0}?{1}", + // Uri.EscapeDataString(className), + // AVClient.BuildQueryString(parameters)), + // method: "GET", + // sessionToken: sessionToken, + // data: null); + + // return commandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).OnSuccess(t => + // { + // return t.Result.Item2; + // }); + //} + } +} diff --git a/Storage/Source/Internal/Query/Controller/IAVQueryController.cs b/Storage/Source/Internal/Query/Controller/IAVQueryController.cs new file mode 100644 index 0000000..f5d1494 --- /dev/null +++ b/Storage/Source/Internal/Query/Controller/IAVQueryController.cs @@ -0,0 +1,21 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace LeanCloud.Storage.Internal { + public interface IAVQueryController { + Task> FindAsync(AVQuery query, + AVUser user, + CancellationToken cancellationToken) where T : AVObject; + + Task CountAsync(AVQuery query, + AVUser user, + CancellationToken cancellationToken) where T : AVObject; + + Task FirstAsync(AVQuery query, + AVUser user, + CancellationToken cancellationToken) where T : AVObject; + } +} diff --git a/Storage/Source/Internal/Session/Controller/AVSessionController.cs b/Storage/Source/Internal/Session/Controller/AVSessionController.cs new file mode 100644 index 0000000..fd40711 --- /dev/null +++ b/Storage/Source/Internal/Session/Controller/AVSessionController.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using LeanCloud.Storage.Internal; + +namespace LeanCloud.Storage.Internal { + public class AVSessionController : IAVSessionController { + private readonly IAVCommandRunner commandRunner; + + public AVSessionController(IAVCommandRunner commandRunner) { + this.commandRunner = commandRunner; + } + + public Task GetSessionAsync(string sessionToken, CancellationToken cancellationToken) { + var command = new AVCommand("sessions/me", + method: "GET", + sessionToken: sessionToken, + data: null); + + return commandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).OnSuccess(t => { + return AVObjectCoder.Instance.Decode(t.Result.Item2, AVDecoder.Instance); + }); + } + + public Task RevokeAsync(string sessionToken, CancellationToken cancellationToken) { + var command = new AVCommand("logout", + method: "POST", + sessionToken: sessionToken, + data: new Dictionary()); + + return commandRunner.RunCommandAsync(command, cancellationToken: cancellationToken); + } + + public Task UpgradeToRevocableSessionAsync(string sessionToken, CancellationToken cancellationToken) { + var command = new AVCommand("upgradeToRevocableSession", + method: "POST", + sessionToken: sessionToken, + data: new Dictionary()); + + return commandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).OnSuccess(t => { + return AVObjectCoder.Instance.Decode(t.Result.Item2, AVDecoder.Instance); + }); + } + + public bool IsRevocableSessionToken(string sessionToken) { + return sessionToken.Contains("r:"); + } + } +} diff --git a/Storage/Source/Internal/Session/Controller/IAVSessionController.cs b/Storage/Source/Internal/Session/Controller/IAVSessionController.cs new file mode 100644 index 0000000..72e7e95 --- /dev/null +++ b/Storage/Source/Internal/Session/Controller/IAVSessionController.cs @@ -0,0 +1,15 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace LeanCloud.Storage.Internal { + public interface IAVSessionController { + Task GetSessionAsync(string sessionToken, CancellationToken cancellationToken); + + Task RevokeAsync(string sessionToken, CancellationToken cancellationToken); + + Task UpgradeToRevocableSessionAsync(string sessionToken, CancellationToken cancellationToken); + + bool IsRevocableSessionToken(string sessionToken); + } +} diff --git a/Storage/Source/Internal/Storage/IStorageController.cs b/Storage/Source/Internal/Storage/IStorageController.cs new file mode 100644 index 0000000..b334c92 --- /dev/null +++ b/Storage/Source/Internal/Storage/IStorageController.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace LeanCloud.Storage.Internal { + /// + /// An abstraction for accessing persistent storage in the LeanCloud SDK. + /// + public interface IStorageController { + /// + /// Load the contents of this storage controller asynchronously. + /// + /// + Task> LoadAsync(); + + /// + /// Overwrites the contents of this storage controller asynchronously. + /// + /// + /// + Task> SaveAsync(IDictionary contents); + } + + /// + /// An interface for a dictionary that is persisted to disk asynchronously. + /// + /// They key type of the dictionary. + /// The value type of the dictionary. + public interface IStorageDictionary : IEnumerable> { + int Count { get; } + TValue this[TKey key] { get; } + + IEnumerable Keys { get; } + IEnumerable Values { get; } + + bool ContainsKey(TKey key); + bool TryGetValue(TKey key, out TValue value); + + /// + /// Adds a key to this dictionary, and saves it asynchronously. + /// + /// The key to insert. + /// The value to insert. + /// + Task AddAsync(TKey key, TValue value); + + /// + /// Removes a key from this dictionary, and saves it asynchronously. + /// + /// + /// + Task RemoveAsync(TKey key); + } +} \ No newline at end of file diff --git a/Storage/Source/Internal/Storage/NetCore/StorageController.cs b/Storage/Source/Internal/Storage/NetCore/StorageController.cs new file mode 100644 index 0000000..87d537c --- /dev/null +++ b/Storage/Source/Internal/Storage/NetCore/StorageController.cs @@ -0,0 +1,184 @@ +using System; +using System.Threading.Tasks; +using System.Linq; +using System.Collections.Generic; +using System.Threading; + +namespace LeanCloud.Storage.Internal +{ + /// + /// Implements `IStorageController` for PCL targets, based off of PCLStorage. + /// + public class StorageController : IStorageController + { + private class StorageDictionary : IStorageDictionary + { + private object mutex; + private Dictionary dictionary; + + public StorageDictionary() + { + this.mutex = new Object(); + dictionary = new Dictionary(); + } + + internal Task SaveAsync() + { + string json; + lock (mutex) + { + json = Json.Encode(dictionary); + } + return Task.FromResult(json); + } + + internal Task LoadAsync() + { + return Task.FromResult("{}").ContinueWith(t => + { + string text = t.Result; + Dictionary result = null; + try + { + result = Json.Parse(text) as Dictionary; + } + catch (Exception) + { + // Do nothing, JSON error. Probaby was empty string. + } + + lock (mutex) + { + dictionary = result ?? new Dictionary(); + } + }); + + } + + internal void Update(IDictionary contents) + { + lock (mutex) + { + dictionary = contents.ToDictionary(p => p.Key, p => p.Value); + } + } + + public Task AddAsync(string key, object value) + { + lock (mutex) + { + dictionary[key] = value; + } + return SaveAsync(); + } + + public Task RemoveAsync(string key) + { + lock (mutex) + { + dictionary.Remove(key); + } + return SaveAsync(); + } + + public bool ContainsKey(string key) + { + lock (mutex) + { + return dictionary.ContainsKey(key); + } + } + + public IEnumerable Keys + { + get { lock (mutex) { return dictionary.Keys; } } + } + + public bool TryGetValue(string key, out object value) + { + lock (mutex) + { + return dictionary.TryGetValue(key, out value); + } + } + + public IEnumerable Values + { + get { lock (mutex) { return dictionary.Values; } } + } + + public object this[string key] + { + get { lock (mutex) { return dictionary[key]; } } + } + + public int Count + { + get { lock (mutex) { return dictionary.Count; } } + } + + public IEnumerator> GetEnumerator() + { + lock (mutex) + { + return dictionary.GetEnumerator(); + } + } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + lock (mutex) + { + return dictionary.GetEnumerator(); + } + } + } + + private const string AVStorageFileName = "ApplicationSettings"; + private readonly TaskQueue taskQueue = new TaskQueue(); + + private StorageDictionary storageDictionary; + + public StorageController() + { + + } + public StorageController(string appId) + { + + } + public Task> LoadAsync() + { + return taskQueue.Enqueue(toAwait => + { + return toAwait.ContinueWith(_ => + { + if (storageDictionary != null) + { + return Task.FromResult>(storageDictionary); + } + + storageDictionary = new StorageDictionary(); + return storageDictionary.LoadAsync().OnSuccess(__ => storageDictionary as IStorageDictionary); + }).Unwrap(); + }, CancellationToken.None); + } + + public Task> SaveAsync(IDictionary contents) + { + return taskQueue.Enqueue(toAwait => + { + return toAwait.ContinueWith(_ => + { + if (storageDictionary == null) + { + storageDictionary = new StorageDictionary(); + } + + storageDictionary.Update(contents); + return storageDictionary.SaveAsync().OnSuccess(__ => storageDictionary as IStorageDictionary); + }).Unwrap(); + }, CancellationToken.None); + } + } +} diff --git a/Storage/Source/Internal/Storage/Portable/StorageController.cs b/Storage/Source/Internal/Storage/Portable/StorageController.cs new file mode 100644 index 0000000..0390ed7 --- /dev/null +++ b/Storage/Source/Internal/Storage/Portable/StorageController.cs @@ -0,0 +1,192 @@ +using System; +using System.Threading.Tasks; +using System.Linq; +using PCLStorage; +using System.Collections.Generic; +using System.Threading; + +namespace LeanCloud.Storage.Internal +{ + /// + /// Implements `IStorageController` for PCL targets, based off of PCLStorage. + /// + public class StorageController : IStorageController + { + private class StorageDictionary : IStorageDictionary + { + private object mutex; + private Dictionary dictionary; + private IFile file; + + public StorageDictionary(IFile file) + { + this.file = file; + + mutex = new Object(); + dictionary = new Dictionary(); + } + + internal Task SaveAsync() + { + string json; + lock (mutex) + { + json = Json.Encode(dictionary); + } + return file.WriteAllTextAsync(json); + } + + internal Task LoadAsync() + { + return file.ReadAllTextAsync().ContinueWith(t => + { + string text = t.Result; + Dictionary result = null; + try + { + result = Json.Parse(text) as Dictionary; + } + catch (Exception) + { + // Do nothing, JSON error. Probaby was empty string. + } + + lock (mutex) + { + dictionary = result ?? new Dictionary(); + } + }); + } + + internal void Update(IDictionary contents) + { + lock (mutex) + { + dictionary = contents.ToDictionary(p => p.Key, p => p.Value); + } + } + + public Task AddAsync(string key, object value) + { + lock (mutex) + { + dictionary[key] = value; + } + return SaveAsync(); + } + + public Task RemoveAsync(string key) + { + lock (mutex) + { + dictionary.Remove(key); + } + return SaveAsync(); + } + + public bool ContainsKey(string key) + { + lock (mutex) + { + return dictionary.ContainsKey(key); + } + } + + public IEnumerable Keys + { + get { lock (mutex) { return dictionary.Keys; } } + } + + public bool TryGetValue(string key, out object value) + { + lock (mutex) + { + return dictionary.TryGetValue(key, out value); + } + } + + public IEnumerable Values + { + get { lock (mutex) { return dictionary.Values; } } + } + + public object this[string key] + { + get { lock (mutex) { return dictionary[key]; } } + } + + public int Count + { + get { lock (mutex) { return dictionary.Count; } } + } + + public IEnumerator> GetEnumerator() + { + lock (mutex) + { + return dictionary.GetEnumerator(); + } + } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + lock (mutex) + { + return dictionary.GetEnumerator(); + } + } + } + + private const string LeanCloudStorageFileName = "ApplicationSettings"; + private readonly TaskQueue taskQueue = new TaskQueue(); + private readonly Task fileTask; + private StorageDictionary storageDictionary; + + public StorageController(string fileNamePrefix) + { + fileTask = taskQueue.Enqueue(t => t.ContinueWith(_ => + { + return FileSystem.Current.LocalStorage.CreateFileAsync(string.Format("{0}_{1}", fileNamePrefix, LeanCloudStorageFileName), CreationCollisionOption.OpenIfExists); + }).Unwrap(), CancellationToken.None); + } + + internal StorageController(IFile file) + { + this.fileTask = Task.FromResult(file); + } + + public Task> LoadAsync() + { + return taskQueue.Enqueue(toAwait => + { + return toAwait.ContinueWith(_ => + { + if (storageDictionary != null) + { + return Task.FromResult>(storageDictionary); + } + + storageDictionary = new StorageDictionary(fileTask.Result); + return storageDictionary.LoadAsync().OnSuccess(__ => storageDictionary as IStorageDictionary); + }).Unwrap(); + }, CancellationToken.None); + } + + public Task> SaveAsync(IDictionary contents) + { + return taskQueue.Enqueue(toAwait => + { + return toAwait.ContinueWith(_ => + { + if (storageDictionary == null) + { + storageDictionary = new StorageDictionary(fileTask.Result); + } + + storageDictionary.Update(contents); + return storageDictionary.SaveAsync().OnSuccess(__ => storageDictionary as IStorageDictionary); + }).Unwrap(); + }, CancellationToken.None); + } + } +} diff --git a/Storage/Source/Internal/Storage/Unity/StorageController.cs b/Storage/Source/Internal/Storage/Unity/StorageController.cs new file mode 100644 index 0000000..432a760 --- /dev/null +++ b/Storage/Source/Internal/Storage/Unity/StorageController.cs @@ -0,0 +1,250 @@ +using System; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using UnityEngine; +using System.Threading; + +namespace LeanCloud.Storage.Internal +{ + /// + /// Implements `IStorageController` for PCL targets, based off of PCLStorage. + /// + public class StorageController : IStorageController + { + private const string LeanCloudStorageFileName = "LeanCloud.settings"; + + private TaskQueue taskQueue = new TaskQueue(); + private string settingsPath; + private StorageDictionary storageDictionary; + private bool isWebPlayer; + + private class StorageDictionary : IStorageDictionary + { + private object mutex; + private Dictionary dictionary; + private string settingsPath; + private bool isWebPlayer; + + public StorageDictionary(string settingsPath, bool isWebPlayer) + { + this.settingsPath = settingsPath; + this.isWebPlayer = isWebPlayer; + + mutex = new object(); + dictionary = new Dictionary(); + } + + internal Task SaveAsync() + { + string jsonEncoded; + lock (mutex) + { + jsonEncoded = Json.Encode(dictionary); + } + + if (this.isWebPlayer) + { + PlayerPrefs.SetString(LeanCloudStorageFileName, jsonEncoded); + PlayerPrefs.Save(); + } + else if (Application.platform == RuntimePlatform.tvOS) + { + Debug.Log("Running on TvOS, prefs cannot be saved."); + } + else + { + using (var fs = new FileStream(settingsPath, FileMode.Create, FileAccess.Write)) + { + using (var writer = new StreamWriter(fs)) + { + writer.Write(jsonEncoded); + } + } + } + + return Task.FromResult(null); + } + + internal Task LoadAsync() + { + string jsonString = null; + + try + { + if (this.isWebPlayer) + { + jsonString = PlayerPrefs.GetString(LeanCloudStorageFileName, null); + } + else if (Application.platform == RuntimePlatform.tvOS) + { + Debug.Log("Running on TvOS, prefs cannot be loaded."); + } + else + { + using (var fs = new FileStream(settingsPath, FileMode.Open, FileAccess.Read)) + { + var reader = new StreamReader(fs); + jsonString = reader.ReadToEnd(); + } + } + } + catch (Exception) + { + // Do nothing + } + + if (jsonString == null) + { + lock (mutex) + { + dictionary = new Dictionary(); + return Task.FromResult(null); + } + } + + Dictionary decoded = Json.Parse(jsonString) as Dictionary; + lock (mutex) + { + dictionary = decoded ?? new Dictionary(); + return Task.FromResult(null); + } + } + + internal void Update(IDictionary contents) + { + lock (mutex) + { + dictionary = contents.ToDictionary(p => p.Key, p => p.Value); + } + } + + public Task AddAsync(string key, object value) + { + lock (mutex) + { + dictionary[key] = value; + } + return SaveAsync(); + } + + public Task RemoveAsync(string key) + { + lock (mutex) + { + dictionary.Remove(key); + } + return SaveAsync(); + } + + public bool ContainsKey(string key) + { + lock (mutex) + { + return dictionary.ContainsKey(key); + } + } + + public IEnumerable Keys + { + get { lock (mutex) { return dictionary.Keys; } } + } + + public bool TryGetValue(string key, out object value) + { + lock (mutex) + { + return dictionary.TryGetValue(key, out value); + } + } + + public IEnumerable Values + { + get { lock (mutex) { return dictionary.Values; } } + } + + public object this[string key] + { + get { lock (mutex) { return dictionary[key]; } } + } + + public int Count + { + get { lock (mutex) { return dictionary.Count; } } + } + + public IEnumerator> GetEnumerator() + { + lock (mutex) + { + return dictionary.GetEnumerator(); + } + } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + lock (mutex) + { + return dictionary.GetEnumerator(); + } + } + } + + public StorageController() + : this(Path.Combine(Application.persistentDataPath, LeanCloudStorageFileName), false) + { + + } + + public StorageController(String settingsPath) + : this(settingsPath, false) + { + + } + public StorageController(bool isWebPlayer, string fileNamePrefix) + : this(Path.Combine(Application.persistentDataPath, string.Format("{0}_{1}", fileNamePrefix, LeanCloudStorageFileName)), isWebPlayer) + { + + } + + public StorageController(string settingsPath, bool isWebPlayer) + { + this.settingsPath = settingsPath; + this.isWebPlayer = isWebPlayer; + } + + public Task> LoadAsync() + { + return taskQueue.Enqueue(toAwait => + { + return toAwait.ContinueWith(_ => + { + if (storageDictionary != null) + { + return Task.FromResult>(storageDictionary); + } + storageDictionary = new StorageDictionary(settingsPath, this.isWebPlayer); + return storageDictionary.LoadAsync().OnSuccess(__ => storageDictionary as IStorageDictionary); + }).Unwrap(); + }, CancellationToken.None); + } + + public Task> SaveAsync(IDictionary contents) + { + return taskQueue.Enqueue(toAwait => + { + return toAwait.ContinueWith(_ => + { + if (storageDictionary == null) + { + storageDictionary = new StorageDictionary(settingsPath, this.isWebPlayer); + } + + storageDictionary.Update(contents); + return storageDictionary.SaveAsync().OnSuccess(__ => storageDictionary as IStorageDictionary); + }).Unwrap(); + }, CancellationToken.None); + } + } +} diff --git a/Storage/Source/Internal/User/Controller/AVCurrentUserController.cs b/Storage/Source/Internal/User/Controller/AVCurrentUserController.cs new file mode 100644 index 0000000..eb78961 --- /dev/null +++ b/Storage/Source/Internal/User/Controller/AVCurrentUserController.cs @@ -0,0 +1,159 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Globalization; +using System.Threading; +using System.Threading.Tasks; +using LeanCloud.Storage.Internal; + +namespace LeanCloud.Storage.Internal +{ + public class AVCurrentUserController : IAVCurrentUserController + { + private readonly object mutex = new object(); + private readonly TaskQueue taskQueue = new TaskQueue(); + + private IStorageController storageController; + + public AVCurrentUserController(IStorageController storageController) + { + this.storageController = storageController; + } + + private AVUser currentUser; + public AVUser CurrentUser + { + get + { + lock (mutex) + { + return currentUser; + } + } + set + { + lock (mutex) + { + currentUser = value; + } + } + } + + public Task SetAsync(AVUser user, CancellationToken cancellationToken) + { + Task saveTask = null; + if (user == null) + { + saveTask = storageController + .LoadAsync() + .OnSuccess(t => t.Result.RemoveAsync("CurrentUser")) + .Unwrap(); + } + else + { + var data = user.ServerDataToJSONObjectForSerialization(); + data["objectId"] = user.ObjectId; + if (user.CreatedAt != null) + { + data["createdAt"] = user.CreatedAt.Value.ToString(AVClient.DateFormatStrings.First(), + CultureInfo.InvariantCulture); + } + if (user.UpdatedAt != null) + { + data["updatedAt"] = user.UpdatedAt.Value.ToString(AVClient.DateFormatStrings.First(), + CultureInfo.InvariantCulture); + } + + saveTask = storageController + .LoadAsync() + .OnSuccess(t => t.Result.AddAsync("CurrentUser", Json.Encode(data))) + .Unwrap(); + } + CurrentUser = user; + + return saveTask; + } + + public Task GetAsync(CancellationToken cancellationToken) + { + AVUser cachedCurrent; + + lock (mutex) + { + cachedCurrent = CurrentUser; + } + + if (cachedCurrent != null) + { + return Task.FromResult(cachedCurrent); + } + + return storageController.LoadAsync().OnSuccess(t => + { + object temp; + t.Result.TryGetValue("CurrentUser", out temp); + var userDataString = temp as string; + AVUser user = null; + if (userDataString != null) + { + var userData = Json.Parse(userDataString) as IDictionary; + var state = AVObjectCoder.Instance.Decode(userData, AVDecoder.Instance); + user = AVObject.FromState(state, "_User"); + } + + CurrentUser = user; + return user; + }); + } + + public Task ExistsAsync(CancellationToken cancellationToken) + { + if (CurrentUser != null) + { + return Task.FromResult(true); + } + + return storageController.LoadAsync().OnSuccess(t => t.Result.ContainsKey("CurrentUser")); + } + + public bool IsCurrent(AVUser user) + { + lock (mutex) + { + return CurrentUser == user; + } + } + + public void ClearFromMemory() + { + CurrentUser = null; + } + + public void ClearFromDisk() + { + lock (mutex) + { + ClearFromMemory(); + + storageController.LoadAsync().OnSuccess(t => t.Result.RemoveAsync("CurrentUser")); + } + } + + public Task GetCurrentSessionTokenAsync(CancellationToken cancellationToken) + { + return GetAsync(cancellationToken).OnSuccess(t => + { + var user = t.Result; + return user == null ? null : user.SessionToken; + }); + } + + public Task LogOutAsync(CancellationToken cancellationToken) + { + return GetAsync(cancellationToken).OnSuccess(t => + { + ClearFromDisk(); + }); + } + } +} diff --git a/Storage/Source/Internal/User/Controller/AVUserController.cs b/Storage/Source/Internal/User/Controller/AVUserController.cs new file mode 100644 index 0000000..c74f602 --- /dev/null +++ b/Storage/Source/Internal/User/Controller/AVUserController.cs @@ -0,0 +1,161 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using LeanCloud.Storage.Internal; + +namespace LeanCloud.Storage.Internal +{ + public class AVUserController : IAVUserController + { + private readonly IAVCommandRunner commandRunner; + + public AVUserController(IAVCommandRunner commandRunner) + { + this.commandRunner = commandRunner; + } + + public Task SignUpAsync(IObjectState state, + IDictionary operations, + CancellationToken cancellationToken) + { + var objectJSON = AVObject.ToJSONObjectForSaving(operations); + + var command = new AVCommand("classes/_User", + method: "POST", + data: objectJSON); + + return commandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).OnSuccess(t => + { + var serverState = AVObjectCoder.Instance.Decode(t.Result.Item2, AVDecoder.Instance); + serverState = serverState.MutatedClone(mutableClone => + { + mutableClone.IsNew = true; + }); + return serverState; + }); + } + + public Task LogInAsync(string username, string email, + string password, + CancellationToken cancellationToken) + { + var data = new Dictionary{ + { "password", password} + }; + if (username != null) { + data.Add("username", username); + } + if (email != null) { + data.Add("email", email); + } + + var command = new AVCommand("login", + method: "POST", + data: data); + + return commandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).OnSuccess(t => + { + var serverState = AVObjectCoder.Instance.Decode(t.Result.Item2, AVDecoder.Instance); + serverState = serverState.MutatedClone(mutableClone => + { + mutableClone.IsNew = t.Result.Item1 == System.Net.HttpStatusCode.Created; + }); + return serverState; + }); + } + + public Task LogInAsync(string authType, + IDictionary data, + bool failOnNotExist, + CancellationToken cancellationToken) + { + var authData = new Dictionary(); + authData[authType] = data; + var path = failOnNotExist ? "users?failOnNotExist=true" : "users"; + var command = new AVCommand(path, + method: "POST", + data: new Dictionary { + { "authData", authData} + }); + + return commandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).OnSuccess(t => + { + var serverState = AVObjectCoder.Instance.Decode(t.Result.Item2, AVDecoder.Instance); + serverState = serverState.MutatedClone(mutableClone => + { + mutableClone.IsNew = t.Result.Item1 == System.Net.HttpStatusCode.Created; + }); + return serverState; + }); + } + + public Task GetUserAsync(string sessionToken, CancellationToken cancellationToken) + { + var command = new AVCommand("users/me", + method: "GET", + sessionToken: sessionToken, + data: null); + + return commandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).OnSuccess(t => + { + return AVObjectCoder.Instance.Decode(t.Result.Item2, AVDecoder.Instance); + }); + } + + public Task RequestPasswordResetAsync(string email, CancellationToken cancellationToken) + { + var command = new AVCommand("requestPasswordReset", + method: "POST", + data: new Dictionary { + { "email", email} + }); + + return commandRunner.RunCommandAsync(command, cancellationToken: cancellationToken); + } + + public Task LogInWithParametersAsync(string relativeUrl, IDictionary data, + CancellationToken cancellationToken) + { + var command = new AVCommand(string.Format("{0}", relativeUrl), + method: "POST", + data: data); + + return commandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).OnSuccess(t => + { + var serverState = AVObjectCoder.Instance.Decode(t.Result.Item2, AVDecoder.Instance); + serverState = serverState.MutatedClone(mutableClone => + { + mutableClone.IsNew = t.Result.Item1 == System.Net.HttpStatusCode.Created; + }); + return serverState; + }); + } + + public Task UpdatePasswordAsync(string userId, string sessionToken, string oldPassword, string newPassword, CancellationToken cancellationToken) + { + var command = new AVCommand(String.Format("users/{0}/updatePassword", userId), + method: "PUT", + sessionToken: sessionToken, + data: new Dictionary { + {"old_password", oldPassword}, + {"new_password", newPassword}, + }); + return commandRunner.RunCommandAsync(command, cancellationToken: cancellationToken); + } + + public Task RefreshSessionTokenAsync(string userId, string sessionToken, + CancellationToken cancellationToken) + { + var command = new AVCommand(String.Format("users/{0}/refreshSessionToken", userId), + method: "PUT", + sessionToken: sessionToken, + data: null); + return AVPlugins.Instance.CommandRunner.RunCommandAsync(command).OnSuccess(t => + { + var serverState = AVObjectCoder.Instance.Decode(t.Result.Item2, AVDecoder.Instance); + return serverState; + }); + } + } +} diff --git a/Storage/Source/Internal/User/Controller/IAVCurrentUserController.cs b/Storage/Source/Internal/User/Controller/IAVCurrentUserController.cs new file mode 100644 index 0000000..c7664aa --- /dev/null +++ b/Storage/Source/Internal/User/Controller/IAVCurrentUserController.cs @@ -0,0 +1,11 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace LeanCloud.Storage.Internal { + public interface IAVCurrentUserController : IAVObjectCurrentController { + Task GetCurrentSessionTokenAsync(CancellationToken cancellationToken); + + Task LogOutAsync(CancellationToken cancellationToken); + } +} diff --git a/Storage/Source/Internal/User/Controller/IAVUserController.cs b/Storage/Source/Internal/User/Controller/IAVUserController.cs new file mode 100644 index 0000000..58966a7 --- /dev/null +++ b/Storage/Source/Internal/User/Controller/IAVUserController.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace LeanCloud.Storage.Internal +{ + public interface IAVUserController + { + Task SignUpAsync(IObjectState state, + IDictionary operations, + CancellationToken cancellationToken); + + Task LogInAsync(string username, + string email, + string password, + CancellationToken cancellationToken); + + Task LogInWithParametersAsync(string relativeUrl, + IDictionary data, + CancellationToken cancellationToken); + + Task LogInAsync(string authType, + IDictionary data, + bool failOnNotExist, + CancellationToken cancellationToken); + + Task GetUserAsync(string sessionToken, + CancellationToken cancellationToken); + + Task RequestPasswordResetAsync(string email, + CancellationToken cancellationToken); + + Task UpdatePasswordAsync(string usedId, string sessionToken, + string oldPassword, string newPassword, + CancellationToken cancellationToken); + + Task RefreshSessionTokenAsync(string userId, + string sessionToken, + CancellationToken cancellationToken); + } +} diff --git a/Storage/Source/Internal/Utilities/AVConfigExtensions.cs b/Storage/Source/Internal/Utilities/AVConfigExtensions.cs new file mode 100644 index 0000000..0df396d --- /dev/null +++ b/Storage/Source/Internal/Utilities/AVConfigExtensions.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; + +namespace LeanCloud.Storage.Internal { + /// + /// So here's the deal. We have a lot of internal APIs for AVObject, AVUser, etc. + /// + /// These cannot be 'internal' anymore if we are fully modularizing things out, because + /// they are no longer a part of the same library, especially as we create things like + /// Installation inside push library. + /// + /// So this class contains a bunch of extension methods that can live inside another + /// namespace, which 'wrap' the intenral APIs that already exist. + /// + public static class AVConfigExtensions { + public static AVConfig Create(IDictionary fetchedConfig) { + return new AVConfig(fetchedConfig); + } + } +} diff --git a/Storage/Source/Internal/Utilities/AVFileExtensions.cs b/Storage/Source/Internal/Utilities/AVFileExtensions.cs new file mode 100644 index 0000000..ae502d5 --- /dev/null +++ b/Storage/Source/Internal/Utilities/AVFileExtensions.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; + +namespace LeanCloud.Storage.Internal { + /// + /// So here's the deal. We have a lot of internal APIs for AVObject, AVUser, etc. + /// + /// These cannot be 'internal' anymore if we are fully modularizing things out, because + /// they are no longer a part of the same library, especially as we create things like + /// Installation inside push library. + /// + /// So this class contains a bunch of extension methods that can live inside another + /// namespace, which 'wrap' the intenral APIs that already exist. + /// + public static class AVFileExtensions { + public static AVFile Create(string name, Uri uri, string mimeType = null) { + return new AVFile(name, uri, mimeType); + } + } +} diff --git a/Storage/Source/Internal/Utilities/AVObjectExtensions.cs b/Storage/Source/Internal/Utilities/AVObjectExtensions.cs new file mode 100644 index 0000000..2147049 --- /dev/null +++ b/Storage/Source/Internal/Utilities/AVObjectExtensions.cs @@ -0,0 +1,225 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Linq; +using System.Globalization; +using System.ComponentModel; +using System.Collections; + +namespace LeanCloud.Storage.Internal +{ + /// + /// So here's the deal. We have a lot of internal APIs for AVObject, AVUser, etc. + /// + /// These cannot be 'internal' anymore if we are fully modularizing things out, because + /// they are no longer a part of the same library, especially as we create things like + /// Installation inside push library. + /// + /// So this class contains a bunch of extension methods that can live inside another + /// namespace, which 'wrap' the intenral APIs that already exist. + /// + public static class AVObjectExtensions + { + public static T FromState(IObjectState state, string defaultClassName) where T : AVObject + { + return AVObject.FromState(state, defaultClassName); + } + + public static IObjectState GetState(this AVObject obj) + { + return obj.State; + } + + public static void HandleFetchResult(this AVObject obj, IObjectState serverState) + { + obj.HandleFetchResult(serverState); + } + + public static IDictionary GetCurrentOperations(this AVObject obj) + { + return obj.CurrentOperations; + } + + public static IDictionary Encode(this AVObject obj) + { + return PointerOrLocalIdEncoder.Instance.EncodeAVObject(obj, false); + } + + public static IEnumerable DeepTraversal(object root, bool traverseAVObjects = false, bool yieldRoot = false) + { + return AVObject.DeepTraversal(root, traverseAVObjects, yieldRoot); + } + + public static void SetIfDifferent(this AVObject obj, string key, T value) + { + obj.SetIfDifferent(key, value); + } + + public static IDictionary ServerDataToJSONObjectForSerialization(this AVObject obj) + { + return obj.ServerDataToJSONObjectForSerialization(); + } + + public static void Set(this AVObject obj, string key, object value) + { + obj.Set(key, value); + } + + public static void DisableHooks(this AVObject obj, IEnumerable hookKeys) + { + obj.Set("__ignore_hooks", hookKeys); + } + public static void DisableHook(this AVObject obj, string hookKey) + { + var newList = new List(); + if (obj.ContainsKey("__ignore_hooks")) + { + var hookKeys = obj.Get>("__ignore_hooks"); + newList = hookKeys.ToList(); + } + newList.Add(hookKey); + obj.DisableHooks(newList); + } + + public static void DisableAfterHook(this AVObject obj) + { + obj.DisableAfterSave(); + obj.DisableAfterUpdate(); + obj.DisableAfterDelete(); + } + + public static void DisableBeforeHook(this AVObject obj) + { + obj.DisableBeforeSave(); + obj.DisableBeforeDelete(); + obj.DisableBeforeUpdate(); + } + + public static void DisableBeforeSave(this AVObject obj) + { + obj.DisableHook("beforeSave"); + } + public static void DisableAfterSave(this AVObject obj) + { + obj.DisableHook("afterSave"); + } + public static void DisableBeforeUpdate(this AVObject obj) + { + obj.DisableHook("beforeUpdate"); + } + public static void DisableAfterUpdate(this AVObject obj) + { + obj.DisableHook("afterUpdate"); + } + public static void DisableBeforeDelete(this AVObject obj) + { + obj.DisableHook("beforeDelete"); + } + public static void DisableAfterDelete(this AVObject obj) + { + obj.DisableHook("afterDelete"); + } + + #region on property updated or changed or collection updated + + /// + /// On the property changed. + /// + /// Av object. + /// Property name. + /// Handler. + public static void OnPropertyChanged(this AVObject avObj, string propertyName, PropertyChangedEventHandler handler) + { + avObj.PropertyChanged += (sender, e) => + { + if (e.PropertyName == propertyName) + { + handler(sender, e); + } + }; + } + + /// + /// On the property updated. + /// + /// Av object. + /// Property name. + /// Handler. + public static void OnPropertyUpdated(this AVObject avObj, string propertyName, PropertyUpdatedEventHandler handler) + { + avObj.PropertyUpdated += (sender, e) => + { + if (e.PropertyName == propertyName) + { + handler(sender, e); + } + }; + } + + /// + /// On the property updated. + /// + /// Av object. + /// Property name. + /// Handler. + public static void OnPropertyUpdated(this AVObject avObj, string propertyName, Action handler) + { + avObj.OnPropertyUpdated(propertyName,(object sender, PropertyUpdatedEventArgs e) => + { + handler(e.OldValue, e.NewValue); + }); + } + + /// + /// On the collection property updated. + /// + /// Av object. + /// Property name. + /// Handler. + public static void OnCollectionPropertyUpdated(this AVObject avObj, string propertyName, CollectionPropertyUpdatedEventHandler handler) + { + avObj.CollectionPropertyUpdated += (sender, e) => + { + if (e.PropertyName == propertyName) + { + handler(sender, e); + } + }; + } + + /// + /// On the collection property added. + /// + /// Av object. + /// Property name. + /// Handler. + public static void OnCollectionPropertyAdded(this AVObject avObj, string propertyName, Action handler) + { + avObj.OnCollectionPropertyUpdated(propertyName, (sender, e) => + { + if (e.CollectionAction == NotifyCollectionUpdatedAction.Add) + { + handler(e.NewValues); + } + }); + } + + /// + /// On the collection property removed. + /// + /// Av object. + /// Property name. + /// Handler. + public static void OnCollectionPropertyRemoved(this AVObject avObj, string propertyName, Action handler) + { + avObj.OnCollectionPropertyUpdated(propertyName, (sender, e) => + { + if (e.CollectionAction == NotifyCollectionUpdatedAction.Remove) + { + handler(e.NewValues); + } + }); + } + #endregion + } +} diff --git a/Storage/Source/Internal/Utilities/AVQueryExtensions.cs b/Storage/Source/Internal/Utilities/AVQueryExtensions.cs new file mode 100644 index 0000000..059650a --- /dev/null +++ b/Storage/Source/Internal/Utilities/AVQueryExtensions.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; + +namespace LeanCloud.Storage.Internal { + /// + /// So here's the deal. We have a lot of internal APIs for AVObject, AVUser, etc. + /// + /// These cannot be 'internal' anymore if we are fully modularizing things out, because + /// they are no longer a part of the same library, especially as we create things like + /// Installation inside push library. + /// + /// So this class contains a bunch of extension methods that can live inside another + /// namespace, which 'wrap' the intenral APIs that already exist. + /// + public static class AVQueryExtensions { + public static string GetClassName(this AVQuery query) where T: AVObject { + return query.ClassName; + } + + public static IDictionary BuildParameters(this AVQuery query) where T: AVObject { + return query.BuildParameters(false); + } + + public static object GetConstraint(this AVQuery query, string key) where T : AVObject { + return query.GetConstraint(key); + } + } +} diff --git a/Storage/Source/Internal/Utilities/AVRelationExtensions.cs b/Storage/Source/Internal/Utilities/AVRelationExtensions.cs new file mode 100644 index 0000000..c2d2c85 --- /dev/null +++ b/Storage/Source/Internal/Utilities/AVRelationExtensions.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; + +namespace LeanCloud.Storage.Internal { + /// + /// So here's the deal. We have a lot of internal APIs for AVObject, AVUser, etc. + /// + /// These cannot be 'internal' anymore if we are fully modularizing things out, because + /// they are no longer a part of the same library, especially as we create things like + /// Installation inside push library. + /// + /// So this class contains a bunch of extension methods that can live inside another + /// namespace, which 'wrap' the intenral APIs that already exist. + /// + public static class AVRelationExtensions { + public static AVRelation Create(AVObject parent, string childKey) where T : AVObject { + return new AVRelation(parent, childKey); + } + + public static AVRelation Create(AVObject parent, string childKey, string targetClassName) where T: AVObject { + return new AVRelation(parent, childKey, targetClassName); + } + + public static string GetTargetClassName(this AVRelation relation) where T : AVObject { + return relation.TargetClassName; + } + } +} diff --git a/Storage/Source/Internal/Utilities/AVSessionExtensions.cs b/Storage/Source/Internal/Utilities/AVSessionExtensions.cs new file mode 100644 index 0000000..40a3c3d --- /dev/null +++ b/Storage/Source/Internal/Utilities/AVSessionExtensions.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace LeanCloud.Storage.Internal { + /// + /// So here's the deal. We have a lot of internal APIs for AVObject, AVUser, etc. + /// + /// These cannot be 'internal' anymore if we are fully modularizing things out, because + /// they are no longer a part of the same library, especially as we create things like + /// Installation inside push library. + /// + /// So this class contains a bunch of extension methods that can live inside another + /// namespace, which 'wrap' the intenral APIs that already exist. + /// + public static class AVSessionExtensions { + public static Task UpgradeToRevocableSessionAsync(string sessionToken, CancellationToken cancellationToken) { + return AVSession.UpgradeToRevocableSessionAsync(sessionToken, cancellationToken); + } + + public static Task RevokeAsync(string sessionToken, CancellationToken cancellationToken) { + return AVSession.RevokeAsync(sessionToken, cancellationToken); + } + } +} diff --git a/Storage/Source/Internal/Utilities/AVUserExtensions.cs b/Storage/Source/Internal/Utilities/AVUserExtensions.cs new file mode 100644 index 0000000..936dc37 --- /dev/null +++ b/Storage/Source/Internal/Utilities/AVUserExtensions.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace LeanCloud.Storage.Internal +{ + /// + /// So here's the deal. We have a lot of internal APIs for AVObject, AVUser, etc. + /// + /// These cannot be 'internal' anymore if we are fully modularizing things out, because + /// they are no longer a part of the same library, especially as we create things like + /// Installation inside push library. + /// + /// So this class contains a bunch of extension methods that can live inside another + /// namespace, which 'wrap' the intenral APIs that already exist. + /// + public static class AVUserExtensions + { + + } +} diff --git a/Storage/Source/Internal/Utilities/FlexibleDictionaryWrapper.cs b/Storage/Source/Internal/Utilities/FlexibleDictionaryWrapper.cs new file mode 100644 index 0000000..5ee7d2c --- /dev/null +++ b/Storage/Source/Internal/Utilities/FlexibleDictionaryWrapper.cs @@ -0,0 +1,104 @@ +using System.Collections.Generic; +using System.Linq; +using LeanCloud.Utilities; + +namespace LeanCloud.Storage.Internal { + /// + /// Provides a Dictionary implementation that can delegate to any other + /// dictionary, regardless of its value type. Used for coercion of + /// dictionaries when returning them to users. + /// + /// The resulting type of value in the dictionary. + /// The original type of value in the dictionary. + [Preserve(AllMembers = true, Conditional = false)] + public class FlexibleDictionaryWrapper : IDictionary { + private readonly IDictionary toWrap; + public FlexibleDictionaryWrapper(IDictionary toWrap) { + this.toWrap = toWrap; + } + + public void Add(string key, TOut value) { + toWrap.Add(key, (TIn)Conversion.ConvertTo(value)); + } + + public bool ContainsKey(string key) { + return toWrap.ContainsKey(key); + } + + public ICollection Keys { + get { return toWrap.Keys; } + } + + public bool Remove(string key) { + return toWrap.Remove(key); + } + + public bool TryGetValue(string key, out TOut value) { + TIn outValue; + bool result = toWrap.TryGetValue(key, out outValue); + value = (TOut)Conversion.ConvertTo(outValue); + return result; + } + + public ICollection Values { + get { + return toWrap.Values + .Select(item => (TOut)Conversion.ConvertTo(item)).ToList(); + } + } + + public TOut this[string key] { + get { + return (TOut)Conversion.ConvertTo(toWrap[key]); + } + set { + toWrap[key] = (TIn)Conversion.ConvertTo(value); + } + } + + public void Add(KeyValuePair item) { + toWrap.Add(new KeyValuePair(item.Key, + (TIn)Conversion.ConvertTo(item.Value))); + } + + public void Clear() { + toWrap.Clear(); + } + + public bool Contains(KeyValuePair item) { + return toWrap.Contains(new KeyValuePair(item.Key, + (TIn)Conversion.ConvertTo(item.Value))); + } + + public void CopyTo(KeyValuePair[] array, int arrayIndex) { + var converted = from pair in toWrap + select new KeyValuePair(pair.Key, + (TOut)Conversion.ConvertTo(pair.Value)); + converted.ToList().CopyTo(array, arrayIndex); + } + + public int Count { + get { return toWrap.Count; } + } + + public bool IsReadOnly { + get { return toWrap.IsReadOnly; } + } + + public bool Remove(KeyValuePair item) { + return toWrap.Remove(new KeyValuePair(item.Key, + (TIn)Conversion.ConvertTo(item.Value))); + } + + public IEnumerator> GetEnumerator() { + foreach (var pair in toWrap) { + yield return new KeyValuePair(pair.Key, + (TOut)Conversion.ConvertTo(pair.Value)); + } + } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { + return this.GetEnumerator(); + } + } +} diff --git a/Storage/Source/Internal/Utilities/FlexibleListWrapper.cs b/Storage/Source/Internal/Utilities/FlexibleListWrapper.cs new file mode 100644 index 0000000..3db78d6 --- /dev/null +++ b/Storage/Source/Internal/Utilities/FlexibleListWrapper.cs @@ -0,0 +1,81 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using LeanCloud.Utilities; + +namespace LeanCloud.Storage.Internal { + /// + /// Provides a List implementation that can delegate to any other + /// list, regardless of its value type. Used for coercion of + /// lists when returning them to users. + /// + /// The resulting type of value in the list. + /// The original type of value in the list. + [Preserve(AllMembers = true, Conditional = false)] + public class FlexibleListWrapper : IList { + private IList toWrap; + public FlexibleListWrapper(IList toWrap) { + this.toWrap = toWrap; + } + + public int IndexOf(TOut item) { + return toWrap.IndexOf((TIn)Conversion.ConvertTo(item)); + } + + public void Insert(int index, TOut item) { + toWrap.Insert(index, (TIn)Conversion.ConvertTo(item)); + } + + public void RemoveAt(int index) { + toWrap.RemoveAt(index); + } + + public TOut this[int index] { + get { + return (TOut)Conversion.ConvertTo(toWrap[index]); + } + set { + toWrap[index] = (TIn)Conversion.ConvertTo(value); + } + } + + public void Add(TOut item) { + toWrap.Add((TIn)Conversion.ConvertTo(item)); + } + + public void Clear() { + toWrap.Clear(); + } + + public bool Contains(TOut item) { + return toWrap.Contains((TIn)Conversion.ConvertTo(item)); + } + + public void CopyTo(TOut[] array, int arrayIndex) { + toWrap.Select(item => (TOut)Conversion.ConvertTo(item)) + .ToList().CopyTo(array, arrayIndex); + } + + public int Count { + get { return toWrap.Count; } + } + + public bool IsReadOnly { + get { return toWrap.IsReadOnly; } + } + + public bool Remove(TOut item) { + return toWrap.Remove((TIn)Conversion.ConvertTo(item)); + } + + public IEnumerator GetEnumerator() { + foreach (var item in (IEnumerable)toWrap) { + yield return (TOut)Conversion.ConvertTo(item); + } + } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { + return this.GetEnumerator(); + } + } +} diff --git a/Storage/Source/Internal/Utilities/IJsonConvertible.cs b/Storage/Source/Internal/Utilities/IJsonConvertible.cs new file mode 100644 index 0000000..af29ef1 --- /dev/null +++ b/Storage/Source/Internal/Utilities/IJsonConvertible.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; + +namespace LeanCloud.Storage.Internal { + /// + /// Represents an object that can be converted into JSON. + /// + public interface IJsonConvertible { + /// + /// Converts the object to a data structure that can be converted to JSON. + /// + /// An object to be JSONified. + IDictionary ToJSON(); + } +} diff --git a/Storage/Source/Internal/Utilities/IdentityEqualityComparer.cs b/Storage/Source/Internal/Utilities/IdentityEqualityComparer.cs new file mode 100644 index 0000000..e25f818 --- /dev/null +++ b/Storage/Source/Internal/Utilities/IdentityEqualityComparer.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace LeanCloud.Storage.Internal +{ + /// + /// An equality comparer that uses the object identity (i.e. ReferenceEquals) + /// rather than .Equals, allowing identity to be used for checking equality in + /// ISets and IDictionaries. + /// + public class IdentityEqualityComparer : IEqualityComparer + { + public bool Equals(T x, T y) + { + return object.ReferenceEquals(x, y); + } + + public int GetHashCode(T obj) + { + return RuntimeHelpers.GetHashCode(obj); + } + } +} diff --git a/Storage/Source/Internal/Utilities/InternalExtensions.cs b/Storage/Source/Internal/Utilities/InternalExtensions.cs new file mode 100644 index 0000000..e0228a5 --- /dev/null +++ b/Storage/Source/Internal/Utilities/InternalExtensions.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.ExceptionServices; +using System.Text; +using System.Threading.Tasks; + +namespace LeanCloud.Storage.Internal { + /// + /// Provides helper methods that allow us to use terser code elsewhere. + /// + public static class InternalExtensions { + /// + /// Ensures a task (even null) is awaitable. + /// + /// + /// + /// + public static Task Safe(this Task task) { + return task ?? Task.FromResult(default(T)); + } + + /// + /// Ensures a task (even null) is awaitable. + /// + /// + /// + public static Task Safe(this Task task) { + return task ?? Task.FromResult(null); + } + + public delegate void PartialAccessor(ref T arg); + + public static TValue GetOrDefault(this IDictionary self, + TKey key, + TValue defaultValue) { + TValue value; + if (self.TryGetValue(key, out value)) { + return value; + } + return defaultValue; + } + + public static bool CollectionsEqual(this IEnumerable a, IEnumerable b) { + return Object.Equals(a, b) || + (a != null && b != null && + a.SequenceEqual(b)); + } + + public static Task OnSuccess(this Task task, + Func, TResult> continuation) { + return ((Task)task).OnSuccess(t => continuation((Task)t)); + } + + public static Task OnSuccess(this Task task, Action> continuation) { + return task.OnSuccess((Func, object>)(t => { + continuation(t); + return null; + })); + } + + public static Task OnSuccess(this Task task, + Func continuation) { + return task.ContinueWith(t => { + if (t.IsFaulted) { + var ex = t.Exception.Flatten(); + if (ex.InnerExceptions.Count == 1) { + ExceptionDispatchInfo.Capture(ex.InnerExceptions[0]).Throw(); + } else { + ExceptionDispatchInfo.Capture(ex).Throw(); + } + // Unreachable + return Task.FromResult(default(TResult)); + } else if (t.IsCanceled) { + var tcs = new TaskCompletionSource(); + tcs.SetCanceled(); + return tcs.Task; + } else { + return Task.FromResult(continuation(t)); + } + }).Unwrap(); + } + + public static Task OnSuccess(this Task task, Action continuation) { + return task.OnSuccess((Func)(t => { + continuation(t); + return null; + })); + } + + public static Task WhileAsync(Func> predicate, Func body) { + Func iterate = null; + iterate = () => { + return predicate().OnSuccess(t => { + if (!t.Result) { + return Task.FromResult(0); + } + return body().OnSuccess(_ => iterate()).Unwrap(); + }).Unwrap(); + }; + return iterate(); + } + } +} diff --git a/Storage/Source/Internal/Utilities/Json.cs b/Storage/Source/Internal/Utilities/Json.cs new file mode 100644 index 0000000..8babc7f --- /dev/null +++ b/Storage/Source/Internal/Utilities/Json.cs @@ -0,0 +1,554 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace LeanCloud.Storage.Internal +{ + /// + /// A simple recursive-descent JSON Parser based on the grammar defined at http://www.json.org + /// and http://tools.ietf.org/html/rfc4627 + /// + public class Json + { + /// + /// Place at the start of a regex to force the match to begin wherever the search starts (i.e. + /// anchored at the index of the first character of the search, even when that search starts + /// in the middle of the string). + /// + private static readonly string startOfString = "\\G"; + private static readonly char startObject = '{'; + private static readonly char endObject = '}'; + private static readonly char startArray = '['; + private static readonly char endArray = ']'; + private static readonly char valueSeparator = ','; + private static readonly char nameSeparator = ':'; + private static readonly char[] falseValue = "false".ToCharArray(); + private static readonly char[] trueValue = "true".ToCharArray(); + private static readonly char[] nullValue = "null".ToCharArray(); + private static readonly Regex numberValue = new Regex(startOfString + + @"-?(?:0|[1-9]\d*)(?\.\d+)?(?(?:e|E)(?:-|\+)?\d+)?"); + private static readonly Regex stringValue = new Regex(startOfString + + "\"(?(?:[^\\\\\"]|(?\\\\(?:[\\\\\"/bfnrt]|u[0-9a-fA-F]{4})))*)\"", + RegexOptions.Multiline); + + private static readonly Regex escapePattern = new Regex("\\\\|\"|[\u0000-\u001F]"); + + private class JsonStringParser + { + public string Input { get; private set; } + + public char[] InputAsArray { get; private set; } + + private int currentIndex; + public int CurrentIndex + { + get + { + return currentIndex; + } + } + + public void Skip(int skip) + { + currentIndex += skip; + } + + public JsonStringParser(string input) + { + Input = input; + InputAsArray = input.ToCharArray(); + } + + /// + /// Parses JSON object syntax (e.g. '{}') + /// + internal bool AVObject(out object output) + { + output = null; + int initialCurrentIndex = CurrentIndex; + if (!Accept(startObject)) + { + return false; + } + var dict = new Dictionary(); + while (true) + { + object pairValue; + if (!ParseMember(out pairValue)) + { + break; + } + var pair = pairValue as Tuple; + dict[pair.Item1] = pair.Item2; + if (!Accept(valueSeparator)) + { + break; + } + } + if (!Accept(endObject)) + { + return false; + } + output = dict; + return true; + } + + /// + /// Parses JSON member syntax (e.g. '"keyname" : null') + /// + private bool ParseMember(out object output) + { + output = null; + object key; + if (!ParseString(out key)) + { + return false; + } + if (!Accept(nameSeparator)) + { + return false; + } + object value; + if (!ParseValue(out value)) + { + return false; + } + output = new Tuple((string)key, value); + return true; + } + + /// + /// Parses JSON array syntax (e.g. '[]') + /// + internal bool ParseArray(out object output) + { + output = null; + if (!Accept(startArray)) + { + return false; + } + var list = new List(); + while (true) + { + object value; + if (!ParseValue(out value)) + { + break; + } + list.Add(value); + if (!Accept(valueSeparator)) + { + break; + } + } + if (!Accept(endArray)) + { + return false; + } + output = list; + return true; + } + + /// + /// Parses a value (i.e. the right-hand side of an object member assignment or + /// an element in an array) + /// + private bool ParseValue(out object output) + { + if (Accept(falseValue)) + { + output = false; + return true; + } + else if (Accept(nullValue)) + { + output = null; + return true; + } + else if (Accept(trueValue)) + { + output = true; + return true; + } + return AVObject(out output) || + ParseArray(out output) || + ParseNumber(out output) || + ParseString(out output); + } + + /// + /// Parses a JSON string (e.g. '"foo\u1234bar\n"') + /// + private bool ParseString(out object output) + { + output = null; + Match m; + if (!Accept(stringValue, out m)) + { + return false; + } + // handle escapes: + int offset = 0; + var contentCapture = m.Groups["content"]; + var builder = new StringBuilder(contentCapture.Value); + foreach (Capture escape in m.Groups["escape"].Captures) + { + int index = (escape.Index - contentCapture.Index) - offset; + offset += escape.Length - 1; + builder.Remove(index + 1, escape.Length - 1); + switch (escape.Value[1]) + { + case '\"': + builder[index] = '\"'; + break; + case '\\': + builder[index] = '\\'; + break; + case '/': + builder[index] = '/'; + break; + case 'b': + builder[index] = '\b'; + break; + case 'f': + builder[index] = '\f'; + break; + case 'n': + builder[index] = '\n'; + break; + case 'r': + builder[index] = '\r'; + break; + case 't': + builder[index] = '\t'; + break; + case 'u': + builder[index] = (char)ushort.Parse(escape.Value.Substring(2), NumberStyles.AllowHexSpecifier); + break; + default: + throw new ArgumentException("Unexpected escape character in string: " + escape.Value); + } + } + output = builder.ToString(); + return true; + } + + /// + /// Parses a number. Returns a long if the number is an integer or has an exponent, + /// otherwise returns a double. + /// + private bool ParseNumber(out object output) + { + output = null; + Match m; + if (!Accept(numberValue, out m)) + { + return false; + } + if (m.Groups["frac"].Length > 0 || m.Groups["exp"].Length > 0) + { + // It's a double. + output = double.Parse(m.Value, CultureInfo.InvariantCulture); + return true; + } + else + { + int temp = 0; + if (int.TryParse(m.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out temp)) + { + output = temp; + return true; + } + output = long.Parse(m.Value, CultureInfo.InvariantCulture); + return true; + } + } + + /// + /// Matches the string to a regex, consuming part of the string and returning the match. + /// + private bool Accept(Regex matcher, out Match match) + { + match = matcher.Match(Input, CurrentIndex); + if (match.Success) + { + Skip(match.Length); + } + return match.Success; + } + + /// + /// Find the first occurrences of a character, consuming part of the string. + /// + private bool Accept(char condition) + { + int step = 0; + int strLen = InputAsArray.Length; + int currentStep = currentIndex; + char currentChar; + + // Remove whitespace + while (currentStep < strLen && + ((currentChar = InputAsArray[currentStep]) == ' ' || + currentChar == '\r' || + currentChar == '\t' || + currentChar == '\n')) + { + ++step; + ++currentStep; + } + + bool match = (currentStep < strLen) && (InputAsArray[currentStep] == condition); + if (match) + { + ++step; + ++currentStep; + + // Remove whitespace + while (currentStep < strLen && + ((currentChar = InputAsArray[currentStep]) == ' ' || + currentChar == '\r' || + currentChar == '\t' || + currentChar == '\n')) + { + ++step; + ++currentStep; + } + + Skip(step); + } + return match; + } + + /// + /// Find the first occurrences of a string, consuming part of the string. + /// + private bool Accept(char[] condition) + { + int step = 0; + int strLen = InputAsArray.Length; + int currentStep = currentIndex; + char currentChar; + + // Remove whitespace + while (currentStep < strLen && + ((currentChar = InputAsArray[currentStep]) == ' ' || + currentChar == '\r' || + currentChar == '\t' || + currentChar == '\n')) + { + ++step; + ++currentStep; + } + + bool strMatch = true; + for (int i = 0; currentStep < strLen && i < condition.Length; ++i, ++currentStep) + { + if (InputAsArray[currentStep] != condition[i]) + { + strMatch = false; + break; + } + } + + bool match = (currentStep < strLen) && strMatch; + if (match) + { + Skip(step + condition.Length); + } + return match; + } + } + + /// + /// Parses a JSON-text as defined in http://tools.ietf.org/html/rfc4627, returning an + /// IDictionary<string, object> or an IList<object> depending on whether + /// the value was an array or dictionary. Nested objects also match these types. + /// + public static object Parse(string input) + { + object output; + input = input.Trim(); + JsonStringParser parser = new JsonStringParser(input); + + if ((parser.AVObject(out output) || + parser.ParseArray(out output)) && + parser.CurrentIndex == input.Length) + { + return output; + } + throw new ArgumentException("Input JSON was invalid."); + } + + /// + /// Encodes a dictionary into a JSON string. Supports values that are + /// IDictionary<string, object>, IList<object>, strings, + /// nulls, and any of the primitive types. + /// + public static string Encode(IDictionary dict) + { + if (dict == null) + { + throw new ArgumentNullException(); + } + if (dict.Count == 0) + { + return "{}"; + } + var builder = new StringBuilder("{"); + foreach (var pair in dict) + { + builder.Append(Encode(pair.Key)); + builder.Append(":"); + builder.Append(Encode(pair.Value)); + builder.Append(","); + } + builder[builder.Length - 1] = '}'; + return builder.ToString(); + } + + /// + /// Encodes a list into a JSON string. Supports values that are + /// IDictionary<string, object>, IList<object>, strings, + /// nulls, and any of the primitive types. + /// + public static string Encode(IList list) + { + if (list == null) + { + throw new ArgumentNullException(); + } + if (list.Count == 0) + { + return "[]"; + } + var builder = new StringBuilder("["); + foreach (var item in list) + { + builder.Append(Encode(item)); + builder.Append(","); + } + builder[builder.Length - 1] = ']'; + return builder.ToString(); + } + + public static string Encode(IList strList) + { + if (strList == null) + { + throw new ArgumentNullException(); + } + if (strList.Count == 0) + { + return "[]"; + } + StringBuilder stringBuilder = new StringBuilder("["); + foreach (object obj in strList) + { + stringBuilder.Append(Json.Encode(obj)); + stringBuilder.Append(","); + } + stringBuilder[stringBuilder.Length - 1] = ']'; + return stringBuilder.ToString(); + } + + public static string Encode(IList> dicList) + { + if (dicList == null) + { + throw new ArgumentNullException(); + } + if (dicList.Count == 0) + { + return "[]"; + } + StringBuilder stringBuilder = new StringBuilder("["); + foreach (object obj in dicList) + { + stringBuilder.Append(Json.Encode(obj)); + stringBuilder.Append(","); + } + stringBuilder[stringBuilder.Length - 1] = ']'; + return stringBuilder.ToString(); + } + + /// + /// Encodes an object into a JSON string. + /// + public static string Encode(object obj) + { + var dict = obj as IDictionary; + if (dict != null) + { + return Encode(dict); + } + var list = obj as IList; + if (list != null) + { + return Encode(list); + } + var dicList = obj as IList>; + if (dicList != null) + { + return Encode(dicList); + } + var strLists = obj as IList; + if (strLists != null) + { + return Encode(strLists); + } + var str = obj as string; + if (str != null) + { + str = escapePattern.Replace(str, m => + { + switch (m.Value[0]) + { + case '\\': + return "\\\\"; + case '\"': + return "\\\""; + case '\b': + return "\\b"; + case '\f': + return "\\f"; + case '\n': + return "\\n"; + case '\r': + return "\\r"; + case '\t': + return "\\t"; + default: + return "\\u" + ((ushort)m.Value[0]).ToString("x4"); + } + }); + return "\"" + str + "\""; + } + if (obj == null) + { + return "null"; + } + if (obj is bool) + { + if ((bool)obj) + { + return "true"; + } + else + { + return "false"; + } + } + if (!obj.GetType().GetTypeInfo().IsPrimitive) + { + throw new ArgumentException("Unable to encode objects of type " + obj.GetType()); + } + return Convert.ToString(obj, CultureInfo.InvariantCulture); + } + } +} diff --git a/Storage/Source/Internal/Utilities/LockSet.cs b/Storage/Source/Internal/Utilities/LockSet.cs new file mode 100644 index 0000000..65e188c --- /dev/null +++ b/Storage/Source/Internal/Utilities/LockSet.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace LeanCloud.Storage.Internal { + public class LockSet { + private static readonly ConditionalWeakTable stableIds = + new ConditionalWeakTable(); + private static long nextStableId = 0; + + private readonly IEnumerable mutexes; + + public LockSet(IEnumerable mutexes) { + this.mutexes = (from mutex in mutexes + orderby GetStableId(mutex) + select mutex).ToList(); + } + + public void Enter() { + foreach (var mutex in mutexes) { + Monitor.Enter(mutex); + } + } + + public void Exit() { + foreach (var mutex in mutexes) { + Monitor.Exit(mutex); + } + } + + private static IComparable GetStableId(object mutex) { + lock (stableIds) { + return stableIds.GetValue(mutex, k => nextStableId++); + } + } + } +} diff --git a/Storage/Source/Internal/Utilities/ReflectionHelpers.cs b/Storage/Source/Internal/Utilities/ReflectionHelpers.cs new file mode 100644 index 0000000..55c4833 --- /dev/null +++ b/Storage/Source/Internal/Utilities/ReflectionHelpers.cs @@ -0,0 +1,123 @@ +using System; +using System.Reflection; +using System.Collections.Generic; +using System.Linq; + +namespace LeanCloud.Storage.Internal +{ + public static class ReflectionHelpers + { + public static IEnumerable GetProperties(Type type) + { +#if MONO || UNITY + return type.GetProperties(); +#else + return type.GetRuntimeProperties(); +#endif + } + + public static MethodInfo GetMethod(Type type, string name, Type[] parameters) + { +#if MONO || UNITY + return type.GetMethod(name, parameters); +#else + return type.GetRuntimeMethod(name, parameters); +#endif + } + + public static bool IsPrimitive(Type type) + { +#if MONO || UNITY + return type.IsPrimitive; +#else + return type.GetTypeInfo().IsPrimitive; +#endif + } + + public static IEnumerable GetInterfaces(Type type) + { +#if MONO || UNITY + return type.GetInterfaces(); +#else + return type.GetTypeInfo().ImplementedInterfaces; +#endif + } + + public static bool IsConstructedGenericType(Type type) + { +#if UNITY + return type.IsGenericType && !type.IsGenericTypeDefinition; +#else + return type.IsConstructedGenericType; +#endif + } + + public static IEnumerable GetConstructors(Type type) + { +#if UNITY + BindingFlags searchFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; + return type.GetConstructors(searchFlags); +#else + return type.GetTypeInfo().DeclaredConstructors + .Where(c => (c.Attributes & MethodAttributes.Static) == 0); +#endif + } + + public static Type[] GetGenericTypeArguments(Type type) + { +#if UNITY + return type.GetGenericArguments(); +#else + return type.GenericTypeArguments; +#endif + } + + public static PropertyInfo GetProperty(Type type, string name) + { +#if MONO || UNITY + return type.GetProperty(name); +#else + return type.GetRuntimeProperty(name); +#endif + } + + /// + /// This method helps simplify the process of getting a constructor for a type. + /// A method like this exists in .NET but is not allowed in a Portable Class Library, + /// so we've built our own. + /// + /// + /// + /// + public static ConstructorInfo FindConstructor(this Type self, params Type[] parameterTypes) + { + var constructors = + from constructor in GetConstructors(self) + let parameters = constructor.GetParameters() + let types = from p in parameters select p.ParameterType + where types.SequenceEqual(parameterTypes) + select constructor; + return constructors.SingleOrDefault(); + } + + public static bool IsNullable(Type t) + { + bool isGeneric; +#if UNITY + isGeneric = t.IsGenericType && !t.IsGenericTypeDefinition; +#else + isGeneric = t.IsConstructedGenericType; +#endif + return isGeneric && t.GetGenericTypeDefinition().Equals(typeof(Nullable<>)); + } + + public static IEnumerable GetCustomAttributes(this Assembly assembly) where T : Attribute + { +#if UNITY + return assembly.GetCustomAttributes(typeof(T), false).Select(attr => attr as T); +#else + return CustomAttributeExtensions.GetCustomAttributes(assembly); +#endif + } + } +} diff --git a/Storage/Source/Internal/Utilities/SynchronizedEventHandler.cs b/Storage/Source/Internal/Utilities/SynchronizedEventHandler.cs new file mode 100644 index 0000000..d94f12a --- /dev/null +++ b/Storage/Source/Internal/Utilities/SynchronizedEventHandler.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace LeanCloud.Storage.Internal { + /// + /// Represents an event handler that calls back from the synchronization context + /// that subscribed. + /// Should look like an EventArgs, but may not inherit EventArgs if T is implemented by the Windows team. + /// + public class SynchronizedEventHandler { + private LinkedList> delegates = + new LinkedList>(); + public void Add(Delegate del) { + lock (delegates) { + TaskFactory factory; + if (SynchronizationContext.Current != null) { + factory = + new TaskFactory(CancellationToken.None, + TaskCreationOptions.None, + TaskContinuationOptions.ExecuteSynchronously, + TaskScheduler.FromCurrentSynchronizationContext()); + } else { + factory = Task.Factory; + } + foreach (var d in del.GetInvocationList()) { + delegates.AddLast(new Tuple(d, factory)); + } + } + } + + public void Remove(Delegate del) { + lock (delegates) { + if (delegates.Count == 0) { + return; + } + foreach (var d in del.GetInvocationList()) { + var node = delegates.First; + while (node != null) { + if (node.Value.Item1 == d) { + delegates.Remove(node); + break; + } + node = node.Next; + } + } + } + } + + public Task Invoke(object sender, T args) { + IEnumerable> toInvoke; + var toContinue = new[] { Task.FromResult(0) }; + lock (delegates) { + toInvoke = delegates.ToList(); + } + var invocations = toInvoke + .Select(p => p.Item2.ContinueWhenAll(toContinue, + _ => p.Item1.DynamicInvoke(sender, args))) + .ToList(); + return Task.WhenAll(invocations); + } + } +} diff --git a/Storage/Source/Internal/Utilities/TaskQueue.cs b/Storage/Source/Internal/Utilities/TaskQueue.cs new file mode 100644 index 0000000..4f1ee25 --- /dev/null +++ b/Storage/Source/Internal/Utilities/TaskQueue.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace LeanCloud.Storage.Internal { + /// + /// A helper class for enqueuing tasks + /// + public class TaskQueue { + /// + /// We only need to keep the tail of the queue. Cancelled tasks will + /// just complete normally/immediately when their turn arrives. + /// + private Task tail; + private readonly object mutex = new object(); + + /// + /// Gets a cancellable task that can be safely awaited and is dependent + /// on the current tail of the queue. This essentially gives us a proxy + /// for the tail end of the queue whose awaiting can be cancelled. + /// + /// A cancellation token that cancels + /// the task even if the task is still in the queue. This allows the + /// running task to return immediately without breaking the dependency + /// chain. It also ensures that errors do not propagate. + /// A new task that should be awaited by enqueued tasks. + private Task GetTaskToAwait(CancellationToken cancellationToken) { + lock (mutex) { + Task toAwait = tail ?? Task.FromResult(true); + return toAwait.ContinueWith(task => { }, cancellationToken); + } + } + + /// + /// Enqueues a task created by . If the task is + /// cancellable (or should be able to be cancelled while it is waiting in the + /// queue), pass a cancellationToken. + /// + /// The type of task. + /// A function given a task to await once state is + /// snapshotted (e.g. after capturing session tokens at the time of the save call). + /// Awaiting this task will wait for the created task's turn in the queue. + /// A cancellation token that can be used to + /// cancel waiting in the queue. + /// The task created by the taskStart function. + public T Enqueue(Func taskStart, CancellationToken cancellationToken) + where T : Task { + Task oldTail; + T task; + lock (mutex) { + oldTail = this.tail ?? Task.FromResult(true); + // The task created by taskStart is responsible for waiting the + // task passed to it before doing its work (this gives it an opportunity + // to do startup work or save state before waiting for its turn in the queue + task = taskStart(GetTaskToAwait(cancellationToken)); + + // The tail task should be dependent on the old tail as well as the newly-created + // task. This prevents cancellation of the new task from causing the queue to run + // out of order. + this.tail = Task.WhenAll(oldTail, task); + } + return task; + } + + public object Mutex { get { return mutex; } } + } +} diff --git a/Storage/Source/Internal/Utilities/XamarinAttributes.cs b/Storage/Source/Internal/Utilities/XamarinAttributes.cs new file mode 100644 index 0000000..219cb1a --- /dev/null +++ b/Storage/Source/Internal/Utilities/XamarinAttributes.cs @@ -0,0 +1,426 @@ +using System; +using System.Collections.Generic; +using System.Threading; + +namespace LeanCloud.Storage.Internal { + /// + /// A reimplementation of Xamarin's PreserveAttribute. + /// This allows us to support AOT and linking for Xamarin platforms. + /// + [AttributeUsage(AttributeTargets.All)] + internal class PreserveAttribute : Attribute { + public bool AllMembers; + public bool Conditional; + } + + [AttributeUsage(AttributeTargets.All)] + internal class LinkerSafeAttribute : Attribute { + public LinkerSafeAttribute() { } + } + + [Preserve(AllMembers = true)] + internal class PreserveWrapperTypes { + /// + /// Exists to ensure that generic types are AOT-compiled for the conversions we support. + /// Any new value types that we add support for will need to be registered here. + /// The method itself is never called, but by virtue of the Preserve attribute being set + /// on the class, these types will be AOT-compiled. + /// + /// This also applies to Unity. + /// + private static List CreateWrapperTypes() { + return new List { + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + + (Action)(() => AVCloud.CallFunctionAsync(null, null, null,CancellationToken.None)), + (Action)(() => AVCloud.CallFunctionAsync(null, null,null, CancellationToken.None)), + (Action)(() => AVCloud.CallFunctionAsync(null, null,null, CancellationToken.None)), + (Action)(() => AVCloud.CallFunctionAsync(null, null,null ,CancellationToken.None)), + (Action)(() => AVCloud.CallFunctionAsync(null, null,null, CancellationToken.None)), + (Action)(() => AVCloud.CallFunctionAsync(null, null,null, CancellationToken.None)), + (Action)(() => AVCloud.CallFunctionAsync(null, null,null, CancellationToken.None)), + (Action)(() => AVCloud.CallFunctionAsync(null, null,null, CancellationToken.None)), + (Action)(() => AVCloud.CallFunctionAsync(null, null, null,CancellationToken.None)), + (Action)(() => AVCloud.CallFunctionAsync(null, null,null ,CancellationToken.None)), + (Action)(() => AVCloud.CallFunctionAsync(null, null,null, CancellationToken.None)), + (Action)(() => AVCloud.CallFunctionAsync(null, null,null, CancellationToken.None)), + (Action)(() => AVCloud.CallFunctionAsync(null, null,null, CancellationToken.None)), + (Action)(() => AVCloud.CallFunctionAsync>(null, null,null, CancellationToken.None)), + (Action)(() => AVCloud.CallFunctionAsync>(null, null,null, CancellationToken.None)), + + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + }; + } + } +} diff --git a/Storage/Source/Public/AVACL.cs b/Storage/Source/Public/AVACL.cs new file mode 100644 index 0000000..22544c4 --- /dev/null +++ b/Storage/Source/Public/AVACL.cs @@ -0,0 +1,284 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using LeanCloud.Storage.Internal; + +namespace LeanCloud { + /// + /// A AVACL is used to control which users and roles can access or modify a particular object. Each + /// can have its own AVACL. You can grant read and write permissions + /// separately to specific users, to groups of users that belong to roles, or you can grant permissions + /// to "the public" so that, for example, any user could read a particular object but only a particular + /// set of users could write to that object. + /// + public class AVACL : IJsonConvertible { + private enum AccessKind { + Read, + Write + } + private const string publicName = "*"; + private readonly ICollection readers = new HashSet(); + private readonly ICollection writers = new HashSet(); + + internal AVACL(IDictionary jsonObject) { + readers = new HashSet(from pair in jsonObject + where ((IDictionary)pair.Value).ContainsKey("read") + select pair.Key); + writers = new HashSet(from pair in jsonObject + where ((IDictionary)pair.Value).ContainsKey("write") + select pair.Key); + } + + /// + /// Creates an ACL with no permissions granted. + /// + public AVACL() { + } + + /// + /// Creates an ACL where only the provided user has access. + /// + /// The only user that can read or write objects governed by this ACL. + public AVACL(AVUser owner) { + SetReadAccess(owner, true); + SetWriteAccess(owner, true); + } + + IDictionary IJsonConvertible.ToJSON() { + var result = new Dictionary(); + foreach (var user in readers.Union(writers)) { + var userPermissions = new Dictionary(); + if (readers.Contains(user)) { + userPermissions["read"] = true; + } + if (writers.Contains(user)) { + userPermissions["write"] = true; + } + result[user] = userPermissions; + } + return result; + } + + private void SetAccess(AccessKind kind, string userId, bool allowed) { + if (userId == null) { + throw new ArgumentException("Cannot set access for an unsaved user or role."); + } + ICollection target = null; + switch (kind) { + case AccessKind.Read: + target = readers; + break; + case AccessKind.Write: + target = writers; + break; + default: + throw new NotImplementedException("Unknown AccessKind"); + } + if (allowed) { + target.Add(userId); + } else { + target.Remove(userId); + } + } + + private bool GetAccess(AccessKind kind, string userId) { + if (userId == null) { + throw new ArgumentException("Cannot get access for an unsaved user or role."); + } + switch (kind) { + case AccessKind.Read: + return readers.Contains(userId); + case AccessKind.Write: + return writers.Contains(userId); + default: + throw new NotImplementedException("Unknown AccessKind"); + } + } + + /// + /// Gets or sets whether the public is allowed to read this object. + /// + public bool PublicReadAccess { + get { + return GetAccess(AccessKind.Read, publicName); + } + set { + SetAccess(AccessKind.Read, publicName, value); + } + } + + /// + /// Gets or sets whether the public is allowed to write this object. + /// + public bool PublicWriteAccess { + get { + return GetAccess(AccessKind.Write, publicName); + } + set { + SetAccess(AccessKind.Write, publicName, value); + } + } + + /// + /// Sets whether the given user id is allowed to read this object. + /// + /// The objectId of the user. + /// Whether the user has permission. + public void SetReadAccess(string userId, bool allowed) { + SetAccess(AccessKind.Read, userId, allowed); + } + + /// + /// Sets whether the given user is allowed to read this object. + /// + /// The user. + /// Whether the user has permission. + public void SetReadAccess(AVUser user, bool allowed) { + SetReadAccess(user.ObjectId, allowed); + } + + /// + /// Sets whether the given user id is allowed to write this object. + /// + /// The objectId of the user. + /// Whether the user has permission. + public void SetWriteAccess(string userId, bool allowed) { + SetAccess(AccessKind.Write, userId, allowed); + } + + /// + /// Sets whether the given user is allowed to write this object. + /// + /// The user. + /// Whether the user has permission. + public void SetWriteAccess(AVUser user, bool allowed) { + SetWriteAccess(user.ObjectId, allowed); + } + + /// + /// Gets whether the given user id is *explicitly* allowed to read this object. + /// Even if this returns false, the user may still be able to read it if + /// PublicReadAccess is true or a role that the user belongs to has read access. + /// + /// The user objectId to check. + /// Whether the user has access. + public bool GetReadAccess(string userId) { + return GetAccess(AccessKind.Read, userId); + } + + /// + /// Gets whether the given user is *explicitly* allowed to read this object. + /// Even if this returns false, the user may still be able to read it if + /// PublicReadAccess is true or a role that the user belongs to has read access. + /// + /// The user to check. + /// Whether the user has access. + public bool GetReadAccess(AVUser user) { + return GetReadAccess(user.ObjectId); + } + + /// + /// Gets whether the given user id is *explicitly* allowed to write this object. + /// Even if this returns false, the user may still be able to write it if + /// PublicReadAccess is true or a role that the user belongs to has write access. + /// + /// The user objectId to check. + /// Whether the user has access. + public bool GetWriteAccess(string userId) { + return GetAccess(AccessKind.Write, userId); + } + + /// + /// Gets whether the given user is *explicitly* allowed to write this object. + /// Even if this returns false, the user may still be able to write it if + /// PublicReadAccess is true or a role that the user belongs to has write access. + /// + /// The user to check. + /// Whether the user has access. + public bool GetWriteAccess(AVUser user) { + return GetWriteAccess(user.ObjectId); + } + + /// + /// Sets whether users belonging to the role with the given + /// are allowed to read this object. + /// + /// The name of the role. + /// Whether the role has access. + public void SetRoleReadAccess(string roleName, bool allowed) { + SetAccess(AccessKind.Read, "role:" + roleName, allowed); + } + + /// + /// Sets whether users belonging to the given role are allowed to read this object. + /// + /// The role. + /// Whether the role has access. + public void SetRoleReadAccess(AVRole role, bool allowed) { + SetRoleReadAccess(role.Name, allowed); + } + + /// + /// Gets whether users belonging to the role with the given + /// are allowed to read this object. Even if this returns false, the role may still be + /// able to read it if a parent role has read access. + /// + /// The name of the role. + /// Whether the role has access. + public bool GetRoleReadAccess(string roleName) { + return GetAccess(AccessKind.Read, "role:" + roleName); + } + + /// + /// Gets whether users belonging to the role are allowed to read this object. + /// Even if this returns false, the role may still be able to read it if a + /// parent role has read access. + /// + /// The name of the role. + /// Whether the role has access. + public bool GetRoleReadAccess(AVRole role) { + return GetRoleReadAccess(role.Name); + } + + /// + /// Sets whether users belonging to the role with the given + /// are allowed to write this object. + /// + /// The name of the role. + /// Whether the role has access. + public void SetRoleWriteAccess(string roleName, bool allowed) { + SetAccess(AccessKind.Write, "role:" + roleName, allowed); + } + + /// + /// Sets whether users belonging to the given role are allowed to write this object. + /// + /// The role. + /// Whether the role has access. + public void SetRoleWriteAccess(AVRole role, bool allowed) { + SetRoleWriteAccess(role.Name, allowed); + } + + /// + /// Gets whether users belonging to the role with the given + /// are allowed to write this object. Even if this returns false, the role may still be + /// able to write it if a parent role has write access. + /// + /// The name of the role. + /// Whether the role has access. + public bool GetRoleWriteAccess(string roleName) { + return GetAccess(AccessKind.Write, "role:" + roleName); + } + + /// + /// Gets whether users belonging to the role are allowed to write this object. + /// Even if this returns false, the role may still be able to write it if a + /// parent role has write access. + /// + /// The name of the role. + /// Whether the role has access. + public bool GetRoleWriteAccess(AVRole role) { + return GetRoleWriteAccess(role.Name); + } + } +} diff --git a/Storage/Source/Public/AVClassNameAttribute.cs b/Storage/Source/Public/AVClassNameAttribute.cs new file mode 100644 index 0000000..aa82d0b --- /dev/null +++ b/Storage/Source/Public/AVClassNameAttribute.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LeanCloud +{ + /// + /// Defines the class name for a subclass of AVObject. + /// + [AttributeUsage(AttributeTargets.Class, Inherited = true, AllowMultiple = false)] + public sealed class AVClassNameAttribute : Attribute + { + /// + /// Constructs a new AVClassName attribute. + /// + /// The class name to associate with the AVObject subclass. + public AVClassNameAttribute(string className) + { + this.ClassName = className; + } + + /// + /// Gets the class name to associate with the AVObject subclass. + /// + public string ClassName { get; private set; } + } +} diff --git a/Storage/Source/Public/AVClient.cs b/Storage/Source/Public/AVClient.cs new file mode 100644 index 0000000..825b791 --- /dev/null +++ b/Storage/Source/Public/AVClient.cs @@ -0,0 +1,503 @@ +using LeanCloud.Storage.Internal; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Net; +using System.Reflection; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace LeanCloud +{ + /// + /// AVClient contains static functions that handle global + /// configuration for the LeanCloud library. + /// + public static partial class AVClient + { + public static readonly string[] DateFormatStrings = { + // Official ISO format + "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'", + // It's possible that the string converter server-side may trim trailing zeroes, + // so these two formats cover ourselves from that. + "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'ff'Z'", + "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'f'Z'", + }; + + /// + /// Represents the configuration of the LeanCloud SDK. + /// + public struct Configuration + { + /// + /// 与 SDK 通讯的云端节点 + /// + public enum AVRegion + { + /// + /// 默认值,LeanCloud 华北节点,同 Public_North_China + /// + [Obsolete("please use Configuration.AVRegion.Public_North_China")] + Public_CN = 0, + + /// + /// 默认值,华北公有云节点,同 Public_CN + /// + Public_North_China = 0, + + /// + /// LeanCloud 北美区公有云节点,同 Public_North_America + /// + [Obsolete("please use Configuration.AVRegion.Public_North_America")] + Public_US = 1, + /// + /// LeanCloud 北美区公有云节点,同 Public_US + /// + Public_North_America = 1, + + /// + /// 华东公有云节点,同 Public_East_China + /// + [Obsolete("please use Configuration.AVRegion.Public_East_China")] + Vendor_Tencent = 2, + + /// + /// 华东公有云节点,同 Vendor_Tencent + /// + Public_East_China = 2, + } + + /// + /// In the event that you would like to use the LeanCloud SDK + /// from a completely portable project, with no platform-specific library required, + /// to get full access to all of our features available on LeanCloud.com + /// (A/B testing, slow queries, etc.), you must set the values of this struct + /// to be appropriate for your platform. + /// + /// Any values set here will overwrite those that are automatically configured by + /// any platform-specific migration library your app includes. + /// + public struct VersionInformation + { + /// + /// The build number of your app. + /// + public String BuildVersion { get; set; } + + /// + /// The human friendly version number of your happ. + /// + public String DisplayVersion { get; set; } + + /// + /// The operating system version of the platform the SDK is operating in.. + /// + public String OSVersion { get; set; } + + } + + /// + /// The LeanCloud application ID of your app. + /// + public String ApplicationId { get; set; } + + /// + /// LeanCloud C# SDK 支持的服务节点,目前支持华北,华东和北美公有云节点和私有节点,以及专属节点 + /// + public AVRegion Region { get; set; } + + internal int RegionValue + { + get + { + return (int)Region; + } + } + + /// + /// The LeanCloud application key for your app. + /// + public String ApplicationKey { get; set; } + + /// + /// The LeanCloud master key for your app. + /// + /// The master key. + public String MasterKey { get; set; } + + /// + /// Gets or sets additional HTTP headers to be sent with network requests from the SDK. + /// + public IDictionary AdditionalHTTPHeaders { get; set; } + + /// + /// The version information of your application environment. + /// + public VersionInformation VersionInfo { get; set; } + + /// + /// Gets or sets the engine server uri. The Uri must have no path port. + /// If this property is set, all LeanEngine cloud func calls will use this server as LeanEngine server. + /// + /// The engine server uri. + public Uri EngineServer { get; set; } + + /// + /// Gets or sets the API server. + /// + /// The API server. + public Uri ApiServer { get; set; } + + /// + /// Gets or sets the push server. + /// + /// The push server. + public Uri PushServer { get; set; } + + /// + /// Gets or sets the stats server. + /// + /// The stats server. + public Uri StatsServer { get; set; } + } + + private static readonly object mutex = new object(); + + static AVClient() + { + versionString = "net-portable-" + Version; + + //AVModuleController.Instance.ScanForModules(); + } + + /// + /// The current configuration that LeanCloud has been initialized with. + /// + public static Configuration CurrentConfiguration { get; internal set; } + + internal static Version Version + { + get + { + var assemblyName = new AssemblyName(typeof(AVClient).GetTypeInfo().Assembly.FullName); + return assemblyName.Version; + } + } + + private static readonly string versionString; + /// + /// 当前 SDK 版本号 + /// + public static string VersionString + { + get + { + return versionString; + } + } + + /// + /// Authenticates this client as belonging to your application. This must be + /// called before your application can use the LeanCloud library. The recommended + /// way is to put a call to AVClient.Initialize in your + /// Application startup. + /// + /// The Application ID provided in the LeanCloud dashboard. + /// + /// The .NET API Key provided in the LeanCloud dashboard. + /// + public static void Initialize(string applicationId, string applicationKey) + { + Initialize(new Configuration + { + ApplicationId = applicationId, + ApplicationKey = applicationKey + }); + } + + internal static Action LogTracker { get; private set; } + + /// + /// 启动日志打印 + /// + /// + public static void HttpLog(Action trace) + { + LogTracker = trace; + } + /// + /// 打印 HTTP 访问日志 + /// + /// + public static void PrintLog(string log) + { + if (AVClient.LogTracker != null) + { + AVClient.LogTracker(log); + } + } + + static bool useProduction = true; + /// + /// Gets or sets a value indicating whether send the request to production server or staging server. + /// + /// true if use production; otherwise, false. + public static bool UseProduction + { + get + { + return useProduction; + } + set + { + useProduction = value; + } + } + + static bool useMasterKey = false; + public static bool UseMasterKey + { + get + { + return useMasterKey; + } + set + { + useMasterKey = value; + } + } + + /// + /// Authenticates this client as belonging to your application. This must be + /// called before your application can use the LeanCloud library. The recommended + /// way is to put a call to AVClient.Initialize in your + /// Application startup. + /// + /// The configuration to initialize LeanCloud with. + /// + public static void Initialize(Configuration configuration) + { + Config(configuration); + + AVObject.RegisterSubclass(); + AVObject.RegisterSubclass(); + AVObject.RegisterSubclass(); + } + + internal static void Config(Configuration configuration) + { + lock (mutex) + { + var nodeHash = configuration.ApplicationId.Split('-'); + if (nodeHash.Length > 1) + { + if (nodeHash[1].Trim() == "9Nh9j0Va") + { + configuration.Region = Configuration.AVRegion.Public_East_China; + } + } + + CurrentConfiguration = configuration; + } + } + + internal static void Clear() + { + AVPlugins.Instance.AppRouterController.Clear(); + AVPlugins.Instance.Reset(); + AVUser.ClearInMemoryUser(); + } + + /// + /// Switch app. + /// + /// Configuration. + public static void Switch(Configuration configuration) + { + Clear(); + Initialize(configuration); + } + + public static void Switch(string applicationId, string applicationKey, Configuration.AVRegion region = Configuration.AVRegion.Public_North_China) + { + var configuration = new Configuration + { + ApplicationId = applicationId, + ApplicationKey = applicationKey, + Region = region + }; + Switch(configuration); + } + + public static string BuildQueryString(IDictionary parameters) + { + return string.Join("&", (from pair in parameters + let valueString = pair.Value as string + select string.Format("{0}={1}", + Uri.EscapeDataString(pair.Key), + Uri.EscapeDataString(string.IsNullOrEmpty(valueString) ? + Json.Encode(pair.Value) : valueString))) + .ToArray()); + } + + internal static IDictionary DecodeQueryString(string queryString) + { + var dict = new Dictionary(); + foreach (var pair in queryString.Split('&')) + { + var parts = pair.Split(new char[] { '=' }, 2); + dict[parts[0]] = parts.Length == 2 ? Uri.UnescapeDataString(parts[1].Replace("+", " ")) : null; + } + return dict; + } + + internal static IDictionary DeserializeJsonString(string jsonData) + { + return Json.Parse(jsonData) as IDictionary; + } + + internal static string SerializeJsonString(IDictionary jsonData) + { + return Json.Encode(jsonData); + } + + public static Task> HttpGetAsync(Uri uri) + { + return RequestAsync(uri, "GET", null, body: null, contentType: null, cancellationToken: CancellationToken.None); + } + + public static Task> RequestAsync(Uri uri, string method, IList> headers, IDictionary body, string contentType, CancellationToken cancellationToken) + { + var dataStream = body != null ? new MemoryStream(Encoding.UTF8.GetBytes(Json.Encode(body))) : null; + return AVClient.RequestAsync(uri, method, headers, dataStream, contentType, cancellationToken); + //return AVPlugins.Instance.HttpClient.ExecuteAsync(request, null, null, cancellationToken); + } + + public static Task> RequestAsync(Uri uri, string method, IList> headers, Stream data, string contentType, CancellationToken cancellationToken) + { + HttpRequest request = new HttpRequest() + { + Data = data != null ? data : null, + Headers = headers, + Method = method, + Uri = uri + }; + return AVPlugins.Instance.HttpClient.ExecuteAsync(request, null, null, cancellationToken).OnSuccess(t => + { + var response = t.Result; + var contentString = response.Item2; + int responseCode = (int)response.Item1; + var responseLog = responseCode + ";" + contentString; + PrintLog(responseLog); + return response; + }); + } + + internal static Tuple> ReponseResolve(Tuple response, CancellationToken cancellationToken) + { + Tuple result = response; + HttpStatusCode code = result.Item1; + string item2 = result.Item2; + + if (item2 == null) + { + cancellationToken.ThrowIfCancellationRequested(); + return new Tuple>(code, null); + } + IDictionary strs = null; + try + { + strs = (!item2.StartsWith("[", StringComparison.Ordinal) ? AVClient.DeserializeJsonString(item2) : new Dictionary() + { + { "results", Json.Parse(item2) } + }); + } + catch (Exception exception) + { + throw new AVException(AVException.ErrorCode.OtherCause, "Invalid response from server", exception); + } + var codeValue = (int)code; + if (codeValue > 203 || codeValue < 200) + { + throw new AVException((AVException.ErrorCode)((int)((strs.ContainsKey("code") ? (long)strs["code"] : (long)-1))), (strs.ContainsKey("error") ? strs["error"] as string : item2), null); + } + + cancellationToken.ThrowIfCancellationRequested(); + return new Tuple>(code, strs); + } + internal static Task>> RequestAsync(string method, Uri relativeUri, string sessionToken, IDictionary data, CancellationToken cancellationToken) + { + + var command = new AVCommand(relativeUri.ToString(), + method: method, + sessionToken: sessionToken, + data: data); + + return AVPlugins.Instance.CommandRunner.RunCommandAsync(command, cancellationToken: cancellationToken); + } + + internal static Task>> RunCommandAsync(AVCommand command) + { + return AVPlugins.Instance.CommandRunner.RunCommandAsync(command); + } + + internal static bool IsSuccessStatusCode(HttpStatusCode responseStatus) + { + var codeValue = (int)responseStatus; + return (codeValue > 199) && (codeValue < 204); + } + + public static string ToLog(this HttpRequest request) + { + StringBuilder sb = new StringBuilder(); + var start = "===HTTP Request Start==="; + sb.AppendLine(start); + var urlLog = "Url: " + request.Uri; + sb.AppendLine(urlLog); + + var methodLog = "Method: " + request.Method; + sb.AppendLine(methodLog); + + try + { + var headers = request.Headers.ToDictionary(x => x.Key, x => x.Value as object); + var headersLog = "Headers: " + Json.Encode(headers); + sb.AppendLine(headersLog); + } + catch (Exception) + { + + } + + try + { + if (request is AVCommand) + { + var command = (AVCommand)request; + if (command.DataObject != null) + { + var bodyLog = "Body:" + Json.Encode(command.DataObject); + sb.AppendLine(bodyLog); + } + } + else + { + StreamReader reader = new StreamReader(request.Data); + string bodyLog = reader.ReadToEnd(); + sb.AppendLine(bodyLog); + } + } + catch (Exception) + { + + } + + var end = "===HTTP Request End==="; + sb.AppendLine(end); + return sb.ToString(); + } + } +} diff --git a/Storage/Source/Public/AVCloud.cs b/Storage/Source/Public/AVCloud.cs new file mode 100644 index 0000000..76b3a13 --- /dev/null +++ b/Storage/Source/Public/AVCloud.cs @@ -0,0 +1,599 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using LeanCloud.Storage.Internal; + +namespace LeanCloud { + /// + /// The AVCloud class provides methods for interacting with LeanCloud Cloud Functions. + /// + /// + /// For example, this sample code calls the + /// "validateGame" Cloud Function and calls processResponse if the call succeeded + /// and handleError if it failed. + /// + /// + /// var result = + /// await AVCloud.CallFunctionAsync<IDictionary<string, object>>("validateGame", parameters); + /// + /// + public static class AVCloud + { + internal static IAVCloudCodeController CloudCodeController + { + get + { + return AVPlugins.Instance.CloudCodeController; + } + } + + /// + /// Calls a cloud function. + /// + /// The type of data you will receive from the cloud function. This + /// can be an IDictionary, string, IList, AVObject, or any other type supported by + /// AVObject. + /// The cloud function to call. + /// The parameters to send to the cloud function. This + /// dictionary can contain anything that could be passed into a AVObject except for + /// AVObjects themselves. + /// + /// The cancellation token. + /// The result of the cloud call. + public static Task CallFunctionAsync(String name, IDictionary parameters = null, string sesstionToken = null, CancellationToken cancellationToken = default(CancellationToken)) + { + var sessionTokenTask = AVUser.TakeSessionToken(sesstionToken); + + return sessionTokenTask.OnSuccess(s => + { + return CloudCodeController.CallFunctionAsync(name, + parameters, s.Result, + cancellationToken); + + }).Unwrap(); + } + + /// + /// 远程调用云函数,返回结果会反序列化为 . + /// + /// + /// + /// + /// + /// + /// + public static Task RPCFunctionAsync(String name, IDictionary parameters = null, string sesstionToken = null, CancellationToken cancellationToken = default(CancellationToken)) + { + var sessionTokenTask = AVUser.TakeSessionToken(sesstionToken); + + return sessionTokenTask.OnSuccess(s => + { + return CloudCodeController.RPCFunction(name, + parameters, + s.Result, + cancellationToken); + }).Unwrap(); + } + + /// + /// 获取 LeanCloud 服务器的时间 + /// + /// 如果获取失败,将返回 DateTime.MinValue + /// + /// + /// 服务器的时间 + public static Task GetServerDateTimeAsync() + { + var command = new AVCommand(relativeUri: "date", + method: "GET", + sessionToken: null, + data: null); + return AVPlugins.Instance.CommandRunner.RunCommandAsync(command).ContinueWith(t => + { + DateTime rtn = DateTime.MinValue; + if (AVClient.IsSuccessStatusCode(t.Result.Item1)) + { + var date = AVDecoder.Instance.Decode(t.Result.Item2); + if (date != null) + { + if (date is DateTime) + { + rtn = (DateTime)date; + } + } + } + return rtn; + }); + } + + /// + /// 请求短信认证。 + /// + /// 手机号。 + /// 应用名称。 + /// 进行的操作名称。 + /// 验证码失效时间。 + /// + public static Task RequestSMSCodeAsync(string mobilePhoneNumber, string name, string op, int ttl = 10) + { + return RequestSMSCodeAsync(mobilePhoneNumber, name, op, ttl, CancellationToken.None); + } + + + /// + /// 请求发送验证码。 + /// + /// 是否发送成功。 + /// 手机号。 + /// 应用名称。 + /// 进行的操作名称。 + /// 验证码失效时间。 + /// Cancellation token。 + public static Task RequestSMSCodeAsync(string mobilePhoneNumber, string name, string op, int ttl = 10, CancellationToken cancellationToken = default(CancellationToken)) + { + if (string.IsNullOrEmpty(mobilePhoneNumber)) + { + throw new AVException(AVException.ErrorCode.MobilePhoneInvalid, "Moblie Phone number is invalid.", null); + } + + Dictionary strs = new Dictionary() + { + { "mobilePhoneNumber", mobilePhoneNumber }, + }; + if (!string.IsNullOrEmpty(name)) + { + strs.Add("name", name); + } + if (!string.IsNullOrEmpty(op)) + { + strs.Add("op", op); + } + if (ttl > 0) + { + strs.Add("TTL", ttl); + } + var command = new AVCommand("requestSmsCode", + method: "POST", + sessionToken: null, + data: strs); + return AVPlugins.Instance.CommandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).ContinueWith(t => + { + return AVClient.IsSuccessStatusCode(t.Result.Item1); + }); + } + + /// + /// 请求发送验证码。 + /// + /// 是否发送成功。 + /// 手机号。 + public static Task RequestSMSCodeAsync(string mobilePhoneNumber) + { + return AVCloud.RequestSMSCodeAsync(mobilePhoneNumber, CancellationToken.None); + } + + + /// + /// 请求发送验证码。 + /// + /// 是否发送成功。 + /// 手机号。 + /// Cancellation Token. + public static Task RequestSMSCodeAsync(string mobilePhoneNumber, CancellationToken cancellationToken) + { + return AVCloud.RequestSMSCodeAsync(mobilePhoneNumber, null, null, 0, cancellationToken); + } + + /// + /// 发送手机短信,并指定模板以及传入模板所需的参数。 + /// Exceptions: + /// AVOSCloud.AVException: + /// 手机号为空。 + /// + /// Sms's template + /// Template variables env. + /// Sms's sign. + /// Cancellation token. + /// + public static Task RequestSMSCodeAsync( + string mobilePhoneNumber, + string template, + IDictionary env, + string sign = "", + string validateToken = "", + CancellationToken cancellationToken = default(CancellationToken)) + { + + if (string.IsNullOrEmpty(mobilePhoneNumber)) + { + throw new AVException(AVException.ErrorCode.MobilePhoneInvalid, "Moblie Phone number is invalid.", null); + } + Dictionary strs = new Dictionary() + { + { "mobilePhoneNumber", mobilePhoneNumber }, + }; + strs.Add("template", template); + if (String.IsNullOrEmpty(sign)) + { + strs.Add("sign", sign); + } + if (String.IsNullOrEmpty(validateToken)) + { + strs.Add("validate_token", validateToken); + } + foreach (var key in env.Keys) + { + strs.Add(key, env[key]); + } + var command = new AVCommand("requestSmsCode", + method: "POST", + sessionToken: null, + data: strs); + return AVPlugins.Instance.CommandRunner.RunCommandAsync(command).ContinueWith(t => + { + return AVClient.IsSuccessStatusCode(t.Result.Item1); + }); + } + + /// + /// + /// + /// + /// + public static Task RequestVoiceCodeAsync(string mobilePhoneNumber) + { + if (string.IsNullOrEmpty(mobilePhoneNumber)) + { + throw new AVException(AVException.ErrorCode.MobilePhoneInvalid, "Moblie Phone number is invalid.", null); + } + Dictionary strs = new Dictionary() + { + { "mobilePhoneNumber", mobilePhoneNumber }, + { "smsType", "voice" }, + { "IDD","+86" } + }; + + var command = new AVCommand("requestSmsCode", + method: "POST", + sessionToken: null, + data: strs); + + return AVPlugins.Instance.CommandRunner.RunCommandAsync(command).ContinueWith(t => + { + return AVClient.IsSuccessStatusCode(t.Result.Item1); + }); + } + + /// + /// 验证是否是有效短信验证码。 + /// + /// 是否验证通过。 + /// 手机号 + /// 验证码。 + public static Task VerifySmsCodeAsync(string code, string mobilePhoneNumber) + { + return AVCloud.VerifySmsCodeAsync(code, mobilePhoneNumber, CancellationToken.None); + } + + /// + /// 验证是否是有效短信验证码。 + /// + /// 是否验证通过。 + /// 验证码。 + /// 手机号 + /// Cancellation token. + public static Task VerifySmsCodeAsync(string code, string mobilePhoneNumber, CancellationToken cancellationToken) + { + var command = new AVCommand("verifySmsCode/" + code.Trim() + "?mobilePhoneNumber=" + mobilePhoneNumber.Trim(), + method: "POST", + sessionToken: null, + data: null); + + return AVPlugins.Instance.CommandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).ContinueWith(t => + { + return AVClient.IsSuccessStatusCode(t.Result.Item1); + }); + } + + /// + /// Stands for a captcha result. + /// + public class Captcha + { + /// + /// Used for captcha verify. + /// + public string Token { internal set; get; } + + /// + /// Captcha image URL. + /// + public string Url { internal set; get; } + + /// + /// Verify the user's input of catpcha. + /// + /// User's input of this captcha. + /// CancellationToken. + /// + public Task VerifyAsync(string code, CancellationToken cancellationToken = default(CancellationToken)) + { + return AVCloud.VerifyCaptchaAsync(code, Token); + } + } + + /// + /// Get a captcha image. + /// + /// captcha image width. + /// captcha image height. + /// CancellationToken. + /// an instance of Captcha. + public static Task RequestCaptchaAsync(int width = 85, int height = 30, CancellationToken cancellationToken = default(CancellationToken)) + { + var path = String.Format("requestCaptcha?width={0}&height={1}", width, height); + var command = new AVCommand(path, method: "GET", sessionToken: null, data: null); + return AVPlugins.Instance.CommandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).OnSuccess(t => + { + var decoded = AVDecoder.Instance.Decode(t.Result.Item2) as IDictionary; + return new Captcha() + { + Token = decoded["captcha_token"] as string, + Url = decoded["captcha_url"] as string, + }; + }); + } + + /// + /// Verify the user's input of catpcha. + /// + /// The captcha's token, from server. + /// User's input of this captcha. + /// CancellationToken. + /// + public static Task VerifyCaptchaAsync(string code, string token, CancellationToken cancellationToken = default(CancellationToken)) + { + var data = new Dictionary + { + { "captcha_token", token }, + { "captcha_code", code }, + }; + var command = new AVCommand("verifyCaptcha", method: "POST", sessionToken: null, data: data); + return AVPlugins.Instance.CommandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).ContinueWith(t => + { + if (!t.Result.Item2.ContainsKey("validate_token")) + throw new KeyNotFoundException("validate_token"); + return t.Result.Item2["validate_token"] as string; + }); + } + + /// + /// Get the custom cloud parameters, you can set them at console https://leancloud.cn/dashboard/devcomponent.html?appid={your_app_Id}#/component/custom_param + /// + /// + /// + public static Task> GetCustomParametersAsync(CancellationToken cancellationToken = default(CancellationToken)) + { + var command = new AVCommand(string.Format("statistics/apps/{0}/sendPolicy", AVClient.CurrentConfiguration.ApplicationId), + method: "GET", + sessionToken: null, + data: null); + + return AVPlugins.Instance.CommandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).OnSuccess(t => + { + var settings = t.Result.Item2; + var CloudParameters = settings["parameters"] as IDictionary; + return CloudParameters; + }); + } + + public class RealtimeSignature + { + public string Nonce { internal set; get; } + public long Timestamp { internal set; get; } + public string ClientId { internal set; get; } + public string Signature { internal set; get; } + } + + public static Task RequestRealtimeSignatureAsync(CancellationToken cancellationToken = default(CancellationToken)) + { + return AVUser.GetCurrentUserAsync(cancellationToken).OnSuccess(t => + { + return RequestRealtimeSignatureAsync(t.Result, cancellationToken); + }).Unwrap(); + } + + public static Task RequestRealtimeSignatureAsync(AVUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + var command = new AVCommand(string.Format("rtm/sign"), + method: "POST", + sessionToken: null, + data: new Dictionary + { + { "session_token", user.SessionToken }, + } + ); + return AVPlugins.Instance.CommandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).ContinueWith(t => + { + var body = t.Result.Item2; + return new RealtimeSignature() + { + Nonce = body["nonce"] as string, + Timestamp = (long)body["timestamp"], + ClientId = body["client_id"] as string, + Signature = body["signature"] as string, + }; + }); + } + + /// + /// Gets the LeanEngine hosting URL for current app async. + /// + /// The lean engine hosting URL async. + public static Task GetLeanEngineHostingUrlAsync() + { + return CallFunctionAsync("_internal_extensions_get_domain"); + } + + } + + + /// + /// AVRPCC loud function base. + /// + public class AVRPCCloudFunctionBase + { + /// + /// AVRPCD eserialize. + /// + public delegate R AVRPCDeserialize(IDictionary result); + /// + /// AVRPCS erialize. + /// + public delegate IDictionary AVRPCSerialize

(P parameters); + + public AVRPCCloudFunctionBase() + : this(true) + { + + } + + public AVRPCCloudFunctionBase(bool noneParameters) + { + if (noneParameters) + { + this.Encode = n => + { + return null; + }; + } + } + + + + private AVRPCDeserialize _decode; + public AVRPCDeserialize Decode + { + get + { + return _decode; + } + set + { + _decode = value; + } + } + + + private AVRPCSerialize

_encode; + public AVRPCSerialize

Encode + { + get + { + if (_encode == null) + { + _encode = n => + { + if (n != null) + return Json.Parse(n.ToString()) as IDictionary; + return null; + }; + } + return _encode; + } + set + { + _encode = value; + } + + } + public string FunctionName { get; set; } + + public Task ExecuteAsync(P parameters) + { + return AVUser.GetCurrentAsync().OnSuccess(t => + { + var user = t.Result; + var encodedParameters = Encode(parameters); + var command = new AVCommand( + string.Format("call/{0}", Uri.EscapeUriString(this.FunctionName)), + method: "POST", + sessionToken: user != null ? user.SessionToken : null, + data: encodedParameters); + + return AVClient.RunCommandAsync(command); + + }).Unwrap().OnSuccess(s => + { + var responseBody = s.Result.Item2; + if (!responseBody.ContainsKey("result")) + { + return default(R); + } + + return Decode(responseBody); + }); + + } + } + + + public class AVObjectRPCCloudFunction : AVObjectRPCCloudFunction + { + + } + public class AVObjectListRPCCloudFunction : AVObjectListRPCCloudFunction + { + + } + + public class AVObjectListRPCCloudFunction : AVRPCCloudFunctionBase> where R : AVObject + { + public AVObjectListRPCCloudFunction() + : base(true) + { + this.Decode = this.AVObjectListDeserializer(); + } + + public AVRPCDeserialize> AVObjectListDeserializer() + { + AVRPCDeserialize> del = data => + { + var items = data["result"] as IList; + + return items.Select(item => + { + var state = AVObjectCoder.Instance.Decode(item as IDictionary, AVDecoder.Instance); + return AVObject.FromState(state, state.ClassName); + }).ToList() as IList; + + }; + return del; + } + } + + public class AVObjectRPCCloudFunction : AVRPCCloudFunctionBase where R : AVObject + { + public AVObjectRPCCloudFunction() + : base(true) + { + this.Decode = this.AVObjectDeserializer(); + } + + + public AVRPCDeserialize AVObjectDeserializer() + { + AVRPCDeserialize del = data => + { + var item = data["result"] as object; + var state = AVObjectCoder.Instance.Decode(item as IDictionary, AVDecoder.Instance); + + return AVObject.FromState(state, state.ClassName); + }; + return del; + + } + } +} diff --git a/Storage/Source/Public/AVConfig.cs b/Storage/Source/Public/AVConfig.cs new file mode 100644 index 0000000..8e3b041 --- /dev/null +++ b/Storage/Source/Public/AVConfig.cs @@ -0,0 +1,143 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using LeanCloud.Storage.Internal; +using LeanCloud.Utilities; + +namespace LeanCloud +{ + /// + /// The AVConfig is a representation of the remote configuration object, + /// that enables you to add things like feature gating, a/b testing or simple "Message of the day". + /// + public class AVConfig : IJsonConvertible + { + private IDictionary properties = new Dictionary(); + + /// + /// Gets the latest fetched AVConfig. + /// + /// AVConfig object + public static AVConfig CurrentConfig + { + get + { + Task task = ConfigController.CurrentConfigController.GetCurrentConfigAsync(); + task.Wait(); + return task.Result; + } + } + + internal static void ClearCurrentConfig() + { + ConfigController.CurrentConfigController.ClearCurrentConfigAsync().Wait(); + } + + internal static void ClearCurrentConfigInMemory() + { + ConfigController.CurrentConfigController.ClearCurrentConfigInMemoryAsync().Wait(); + } + + private static IAVConfigController ConfigController + { + get { return AVPlugins.Instance.ConfigController; } + } + + internal AVConfig() + : base() + { + } + + internal AVConfig(IDictionary fetchedConfig) + { + var props = AVDecoder.Instance.Decode(fetchedConfig["params"]) as IDictionary; + properties = props; + } + + /// + /// Retrieves the AVConfig asynchronously from the server. + /// + /// AVConfig object that was fetched + public static Task GetAsync() + { + return GetAsync(CancellationToken.None); + } + + /// + /// Retrieves the AVConfig asynchronously from the server. + /// + /// The cancellation token. + /// AVConfig object that was fetched + public static Task GetAsync(CancellationToken cancellationToken) + { + return ConfigController.FetchConfigAsync(AVUser.CurrentSessionToken, cancellationToken); + } + + /// + /// Gets a value for the key of a particular type. + /// + /// The type to convert the value to. Supported types are + /// AVObject and its descendents, LeanCloud types such as AVRelation and AVGeopoint, + /// primitive types,IList<T>, IDictionary<string, T> and strings. + /// The key of the element to get. + /// The property is retrieved + /// and is not found. + /// The property under this + /// key was found, but of a different type. + public T Get(string key) + { + return Conversion.To(this.properties[key]); + } + + /// + /// Populates result with the value for the key, if possible. + /// + /// The desired type for the value. + /// The key to retrieve a value for. + /// The value for the given key, converted to the + /// requested type, or null if unsuccessful. + /// true if the lookup and conversion succeeded, otherwise false. + public bool TryGetValue(string key, out T result) + { + if (this.properties.ContainsKey(key)) + { + try + { + var temp = Conversion.To(this.properties[key]); + result = temp; + return true; + } + catch (Exception) + { + // Could not convert, do nothing + } + } + result = default(T); + return false; + } + + /// + /// Gets a value on the config. + /// + /// The key for the parameter. + /// The property is + /// retrieved and is not found. + /// The value for the key. + virtual public object this[string key] + { + get + { + return this.properties[key]; + } + } + + IDictionary IJsonConvertible.ToJSON() + { + return new Dictionary { + { "params", NoObjectsEncoder.Instance.Encode(properties) } + }; + } + } +} diff --git a/Storage/Source/Public/AVDownloadProgressEventArgs.cs b/Storage/Source/Public/AVDownloadProgressEventArgs.cs new file mode 100644 index 0000000..8858eaa --- /dev/null +++ b/Storage/Source/Public/AVDownloadProgressEventArgs.cs @@ -0,0 +1,15 @@ +using System; + +namespace LeanCloud { + /// + /// Represents download progress. + /// + public class AVDownloadProgressEventArgs : EventArgs { + public AVDownloadProgressEventArgs() { } + + /// + /// Gets the progress (a number between 0.0 and 1.0) of a download. + /// + public double Progress { get; set; } + } +} diff --git a/Storage/Source/Public/AVException.cs b/Storage/Source/Public/AVException.cs new file mode 100644 index 0000000..4fedef0 --- /dev/null +++ b/Storage/Source/Public/AVException.cs @@ -0,0 +1,275 @@ +using System; + +namespace LeanCloud +{ + /// + /// Exceptions that may occur when sending requests to LeanCloud. + /// + public class AVException : Exception + { + /// + /// Error codes that may be delivered in response to requests to LeanCloud. + /// + public enum ErrorCode + { + /// + /// Error code indicating that an unknown error or an error unrelated to LeanCloud + /// occurred. + /// + OtherCause = -1, + + /// + /// Error code indicating that something has gone wrong with the server. + /// If you get this error code, it is LeanCloud's fault. + /// + InternalServerError = 1, + + /// + /// Error code indicating the connection to the LeanCloud servers failed. + /// + ConnectionFailed = 100, + + /// + /// Error code indicating the specified object doesn't exist. + /// + ObjectNotFound = 101, + + /// + /// Error code indicating you tried to query with a datatype that doesn't + /// support it, like exact matching an array or object. + /// + InvalidQuery = 102, + + /// + /// Error code indicating a missing or invalid classname. Classnames are + /// case-sensitive. They must start with a letter, and a-zA-Z0-9_ are the + /// only valid characters. + /// + InvalidClassName = 103, + + /// + /// Error code indicating an unspecified object id. + /// + MissingObjectId = 104, + + /// + /// Error code indicating an invalid key name. Keys are case-sensitive. They + /// must start with a letter, and a-zA-Z0-9_ are the only valid characters. + /// + InvalidKeyName = 105, + + /// + /// Error code indicating a malformed pointer. You should not see this unless + /// you have been mucking about changing internal LeanCloud code. + /// + InvalidPointer = 106, + + /// + /// Error code indicating that badly formed JSON was received upstream. This + /// either indicates you have done something unusual with modifying how + /// things encode to JSON, or the network is failing badly. + /// + InvalidJSON = 107, + + /// + /// Error code indicating that the feature you tried to access is only + /// available internally for testing purposes. + /// + CommandUnavailable = 108, + + /// + /// You must call LeanCloud.initialize before using the LeanCloud library. + /// + NotInitialized = 109, + + /// + /// Error code indicating that a field was set to an inconsistent type. + /// + IncorrectType = 111, + + /// + /// Error code indicating an invalid channel name. A channel name is either + /// an empty string (the broadcast channel) or contains only a-zA-Z0-9_ + /// characters and starts with a letter. + /// + InvalidChannelName = 112, + + /// + /// Error code indicating that push is misconfigured. + /// + PushMisconfigured = 115, + + /// + /// Error code indicating that the object is too large. + /// + ObjectTooLarge = 116, + + /// + /// Error code indicating that the operation isn't allowed for clients. + /// + OperationForbidden = 119, + + /// + /// Error code indicating the result was not found in the cache. + /// + CacheMiss = 120, + + /// + /// Error code indicating that an invalid key was used in a nested + /// JSONObject. + /// + InvalidNestedKey = 121, + + /// + /// Error code indicating that an invalid filename was used for AVFile. + /// A valid file name contains only a-zA-Z0-9_. characters and is between 1 + /// and 128 characters. + /// + InvalidFileName = 122, + + /// + /// Error code indicating an invalid ACL was provided. + /// + InvalidACL = 123, + + /// + /// Error code indicating that the request timed out on the server. Typically + /// this indicates that the request is too expensive to run. + /// + Timeout = 124, + + /// + /// Error code indicating that the email address was invalid. + /// + InvalidEmailAddress = 125, + + /// + /// Error code indicating that a unique field was given a value that is + /// already taken. + /// + DuplicateValue = 137, + + /// + /// Error code indicating that a role's name is invalid. + /// + InvalidRoleName = 139, + + /// + /// Error code indicating that an application quota was exceeded. Upgrade to + /// resolve. + /// + ExceededQuota = 140, + + /// + /// Error code indicating that a Cloud Code script failed. + /// + ScriptFailed = 141, + + /// + /// Error code indicating that a Cloud Code validation failed. + /// + ValidationFailed = 142, + + /// + /// Error code indicating that deleting a file failed. + /// + FileDeleteFailed = 153, + + /// + /// Error code indicating that the application has exceeded its request limit. + /// + RequestLimitExceeded = 155, + + /// + /// Error code indicating that the provided event name is invalid. + /// + InvalidEventName = 160, + + /// + /// Error code indicating that the username is missing or empty. + /// + UsernameMissing = 200, + + /// + /// Error code indicating that the password is missing or empty. + /// + PasswordMissing = 201, + + /// + /// Error code indicating that the username has already been taken. + /// + UsernameTaken = 202, + + /// + /// Error code indicating that the email has already been taken. + /// + EmailTaken = 203, + + /// + /// Error code indicating that the email is missing, but must be specified. + /// + EmailMissing = 204, + + /// + /// Error code indicating that a user with the specified email was not found. + /// + EmailNotFound = 205, + + /// + /// Error code indicating that a user object without a valid session could + /// not be altered. + /// + SessionMissing = 206, + + /// + /// Error code indicating that a user can only be created through signup. + /// + MustCreateUserThroughSignup = 207, + + /// + /// Error code indicating that an an account being linked is already linked + /// to another user. + /// + AccountAlreadyLinked = 208, + + /// + /// Error code indicating that the current session token is invalid. + /// + InvalidSessionToken = 209, + + /// + /// Error code indicating that a user cannot be linked to an account because + /// that account's id could not be found. + /// + LinkedIdMissing = 250, + + /// + /// Error code indicating that a user with a linked (e.g. Facebook) account + /// has an invalid session. + /// + InvalidLinkedSession = 251, + + /// + /// Error code indicating that a service being linked (e.g. Facebook or + /// Twitter) is unsupported. + /// + UnsupportedService = 252, + + /// + /// 手机号不合法 + /// + MobilePhoneInvalid = 253 + } + + internal AVException(ErrorCode code, string message, Exception cause = null) + : base(message, cause) + { + this.Code = code; + } + + /// + /// The LeanCloud error code associated with the exception. + /// + public ErrorCode Code { get; private set; } + } +} diff --git a/Storage/Source/Public/AVExtensions.cs b/Storage/Source/Public/AVExtensions.cs new file mode 100644 index 0000000..973ff5c --- /dev/null +++ b/Storage/Source/Public/AVExtensions.cs @@ -0,0 +1,160 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using System.Linq; +using LeanCloud.Storage.Internal; + +namespace LeanCloud +{ + /// + /// Provides convenience extension methods for working with collections + /// of AVObjects so that you can easily save and fetch them in batches. + /// + public static class AVExtensions + { + /// + /// Saves all of the AVObjects in the enumeration. Equivalent to + /// calling . + /// + /// The objects to save. + public static Task SaveAllAsync(this IEnumerable objects) where T : AVObject + { + return AVObject.SaveAllAsync(objects); + } + + /// + /// Saves all of the AVObjects in the enumeration. Equivalent to + /// calling + /// . + /// + /// The objects to save. + /// The cancellation token. + public static Task SaveAllAsync( + this IEnumerable objects, CancellationToken cancellationToken) where T : AVObject + { + return AVObject.SaveAllAsync(objects, cancellationToken); + } + + /// + /// Fetches all of the objects in the enumeration. Equivalent to + /// calling . + /// + /// The objects to save. + public static Task> FetchAllAsync(this IEnumerable objects) + where T : AVObject + { + return AVObject.FetchAllAsync(objects); + } + + /// + /// Fetches all of the objects in the enumeration. Equivalent to + /// calling + /// . + /// + /// The objects to fetch. + /// The cancellation token. + public static Task> FetchAllAsync( + this IEnumerable objects, CancellationToken cancellationToken) + where T : AVObject + { + return AVObject.FetchAllAsync(objects, cancellationToken); + } + + /// + /// Fetches all of the objects in the enumeration that don't already have + /// data. Equivalent to calling + /// . + /// + /// The objects to fetch. + public static Task> FetchAllIfNeededAsync( + this IEnumerable objects) + where T : AVObject + { + return AVObject.FetchAllIfNeededAsync(objects); + } + + /// + /// Fetches all of the objects in the enumeration that don't already have + /// data. Equivalent to calling + /// . + /// + /// The objects to fetch. + /// The cancellation token. + public static Task> FetchAllIfNeededAsync( + this IEnumerable objects, CancellationToken cancellationToken) + where T : AVObject + { + return AVObject.FetchAllIfNeededAsync(objects, cancellationToken); + } + + /// + /// Constructs a query that is the or of the given queries. + /// + /// The type of AVObject being queried. + /// An initial query to 'or' with additional queries. + /// The list of AVQueries to 'or' together. + /// A query that is the or of the given queries. + public static AVQuery Or(this AVQuery source, params AVQuery[] queries) + where T : AVObject + { + return AVQuery.Or(queries.Concat(new[] { source })); + } + + /// + /// Fetches this object with the data from the server. + /// + public static Task FetchAsync(this T obj) where T : AVObject + { + return obj.FetchAsyncInternal(CancellationToken.None).OnSuccess(t => (T)t.Result); + } + + /// + /// Fetches this object with the data from the server. + /// + /// The AVObject to fetch. + /// The cancellation token. + public static Task FetchAsync(this T obj, CancellationToken cancellationToken) + where T : AVObject + { + return FetchAsync(obj, null, cancellationToken); + } + public static Task FetchAsync(this T obj, IEnumerable includeKeys) where T : AVObject + { + return FetchAsync(obj, includeKeys, CancellationToken.None).OnSuccess(t => (T)t.Result); + } + public static Task FetchAsync(this T obj, IEnumerable includeKeys, CancellationToken cancellationToken) + where T : AVObject + { + var queryString = new Dictionary(); + if (includeKeys != null) + { + + var encode = string.Join(",", includeKeys.ToArray()); + queryString.Add("include", encode); + } + return obj.FetchAsyncInternal(queryString, cancellationToken).OnSuccess(t => (T)t.Result); + } + + /// + /// If this AVObject has not been fetched (i.e. returns + /// false), fetches this object with the data from the server. + /// + /// The AVObject to fetch. + public static Task FetchIfNeededAsync(this T obj) where T : AVObject + { + return obj.FetchIfNeededAsyncInternal(CancellationToken.None).OnSuccess(t => (T)t.Result); + } + + /// + /// If this AVObject has not been fetched (i.e. returns + /// false), fetches this object with the data from the server. + /// + /// The AVObject to fetch. + /// The cancellation token. + public static Task FetchIfNeededAsync(this T obj, CancellationToken cancellationToken) + where T : AVObject + { + return obj.FetchIfNeededAsyncInternal(cancellationToken).OnSuccess(t => (T)t.Result); + } + } +} diff --git a/Storage/Source/Public/AVFieldNameAttribute.cs b/Storage/Source/Public/AVFieldNameAttribute.cs new file mode 100644 index 0000000..8ee91a7 --- /dev/null +++ b/Storage/Source/Public/AVFieldNameAttribute.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LeanCloud +{ + /// + /// Specifies a field name for a property on a AVObject subclass. + /// + [AttributeUsage(AttributeTargets.Property, Inherited = true, AllowMultiple = false)] + public sealed class AVFieldNameAttribute : Attribute + { + /// + /// Constructs a new AVFieldName attribute. + /// + /// The name of the field on the AVObject that the + /// property represents. + public AVFieldNameAttribute(string fieldName) + { + FieldName = fieldName; + } + + /// + /// Gets the name of the field represented by this property. + /// + public string FieldName { get; private set; } + } +} diff --git a/Storage/Source/Public/AVFile.cs b/Storage/Source/Public/AVFile.cs new file mode 100644 index 0000000..6d05995 --- /dev/null +++ b/Storage/Source/Public/AVFile.cs @@ -0,0 +1,728 @@ +using LeanCloud.Storage.Internal; +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Threading; +using System.Threading.Tasks; + +namespace LeanCloud +{ + /// + /// AVFile is a local representation of a file that is saved to the LeanCloud. + /// + /// + /// The workflow is to construct a with data and a filename, + /// then save it and set it as a field on a AVObject: + /// + /// + /// var file = new AVFile("hello.txt", + /// new MemoryStream(Encoding.UTF8.GetBytes("hello"))); + /// await file.SaveAsync(); + /// var obj = new AVObject("TestObject"); + /// obj["file"] = file; + /// await obj.SaveAsync(); + /// + /// + public partial class AVFile : IJsonConvertible + { + internal static int objectCounter = 0; + internal static readonly object Mutex = new object(); + private FileState state; + private readonly Stream dataStream; + private readonly TaskQueue taskQueue = new TaskQueue(); + + #region Constructor + /// + /// 通过文件名,数据流,文件类型,文件源信息构建一个 AVFile + /// + /// 文件名 + /// 数据流 + /// 文件类型 + /// 文件源信息 + public AVFile(string name, Stream data, string mimeType = null, IDictionary metaData = null) + { + mimeType = mimeType == null ? GetMIMEType(name) : mimeType; + state = new FileState + { + Name = name, + MimeType = mimeType, + MetaData = metaData + }; + this.dataStream = data; + lock (Mutex) + { + objectCounter++; + state.counter = objectCounter; + } + } + + /// + /// 根据文件名,文件 Byte 数组,以及文件类型构建 AVFile + /// + /// 文件名 + /// 文件 Byte 数组 + /// 文件类型 + public AVFile(string name, byte[] data, string mimeType = null) + : this(name, new MemoryStream(data), mimeType) { } + + /// + /// 根据文件名,文件流数据,文件类型构建 AVFile + /// + /// 文件名 + /// 文件流数据 + /// 文件类型 + public AVFile(string name, Stream data, string mimeType = null) + : this(name, data, mimeType, new Dictionary()) + { + } + + /// + /// 根据 byte 数组以及文件名创建文件 + /// + /// 文件名 + /// 文件的 byte[] 数据 + public AVFile(string name, byte[] data) + : this(name, new MemoryStream(data), new Dictionary()) + { + + } + + /// + /// 根据文件名,数据 byte[] 数组以及元数据创建文件 + /// + /// 文件名 + /// 文件的 byte[] 数据 + /// 元数据 + public AVFile(string name, byte[] data, IDictionary metaData) + : this(name, new MemoryStream(data), metaData) + { + + } + + /// + /// 根据文件名,数据流以及元数据创建文件 + /// + /// 文件名 + /// 文件的数据流 + /// 元数据 + public AVFile(string name, Stream data, IDictionary metaData) + : this(name, data, GetMIMEType(name), metaData) + { + } + + /// + /// 根据文件名,数据流以及元数据创建文件 + /// + /// 文件名 + /// 文件的数据流 + public AVFile(string name, Stream data) + : this(name, data, new Dictionary()) + { + + } + + #region created by url or uri + /// + /// 根据文件名,Uri,文件类型以及文件源信息 + /// + /// 文件名 + /// 文件Uri + /// 文件类型 + /// 文件源信息 + public AVFile(string name, Uri uri, string mimeType = null, IDictionary metaData = null) + { + mimeType = mimeType == null ? GetMIMEType(name) : mimeType; + state = new FileState + { + Name = name, + Url = uri, + MetaData = metaData, + MimeType = mimeType + }; + lock (Mutex) + { + objectCounter++; + state.counter = objectCounter; + } + this.isExternal = true; + } + + /// + /// 根据文件名,文件 Url,文件类型,文件源信息构建 AVFile + /// + /// 文件名 + /// 文件 Url + /// 文件类型 + /// 文件源信息 + public AVFile(string name, string url, string mimeType = null, IDictionary metaData = null) + : this(name, new Uri(url), mimeType, metaData) + { + + } + + /// + /// 根据文件名,文件 Url以及文件的源信息构建 AVFile + /// + /// 文件名 + /// 文件 Url + /// 文件源信息 + public AVFile(string name, string url, IDictionary metaData) + : this(name, url, null, metaData) + { + } + + /// + /// 根据文件名,文件 Uri,以及文件类型构建 AVFile + /// + /// 文件名 + /// 文件 Uri + /// 文件类型 + public AVFile(string name, Uri uri, string mimeType = null) + : this(name, uri, mimeType, new Dictionary()) + { + + } + + /// + /// 根据文件名以及文件 Uri 构建 AVFile + /// + /// 文件名 + /// 文件 Uri + public AVFile(string name, Uri uri) + : this(name, uri, null, new Dictionary()) + { + + } + /// + /// 根据文件名和 Url 创建文件 + /// + /// 文件名 + /// 文件的 Url + public AVFile(string name, string url) + : this(name, new Uri(url)) + { + } + + internal AVFile(FileState filestate) + { + this.state = filestate; + } + internal AVFile(string objectId) + : this(new FileState() + { + ObjectId = objectId + }) + { + + } + #endregion + + #endregion + + #region Properties + + /// + /// Gets whether the file still needs to be saved. + /// + public bool IsDirty + { + get + { + return state.Url == null; + } + } + + /// + /// Gets the name of the file. Before save is called, this is the filename given by + /// the user. After save is called, that name gets prefixed with a unique identifier. + /// + [AVFieldName("name")] + public string Name + { + get + { + return state.Name; + } + } + + /// + /// Gets the MIME type of the file. This is either passed in to the constructor or + /// inferred from the file extension. "unknown/unknown" will be used if neither is + /// available. + /// + public string MimeType + { + get + { + return state.MimeType; + } + } + + /// + /// Gets the url of the file. It is only available after you save the file or after + /// you get the file from a . + /// + [AVFieldName("url")] + public Uri Url + { + get + { + return state.Url; + } + } + + internal static IAVFileController FileController + { + get + { + return AVPlugins.Instance.FileController; + } + } + + #endregion + + IDictionary IJsonConvertible.ToJSON() + { + if (this.IsDirty) + { + throw new InvalidOperationException( + "AVFile must be saved before it can be serialized."); + } + return new Dictionary { + {"__type", "File"}, + { "id", ObjectId }, + {"name", Name}, + {"url", Url.AbsoluteUri} + }; + } + + #region Save + + /// + /// Saves the file to the LeanCloud cloud. + /// + public Task SaveAsync() + { + return SaveAsync(null, CancellationToken.None); + } + + /// + /// Saves the file to the LeanCloud cloud. + /// + /// The cancellation token. + public Task SaveAsync(CancellationToken cancellationToken) + { + return SaveAsync(null, cancellationToken); + } + + /// + /// Saves the file to the LeanCloud cloud. + /// + /// The progress callback. + public Task SaveAsync(IProgress progress) + { + return SaveAsync(progress, CancellationToken.None); + } + + /// + /// Saves the file to the LeanCloud cloud. + /// + /// The progress callback. + /// The cancellation token. + public Task SaveAsync(IProgress progress, + CancellationToken cancellationToken) + { + if (this.isExternal) + return this.SaveExternal(); + + return taskQueue.Enqueue( + toAwait => FileController.SaveAsync(state, dataStream, AVUser.CurrentSessionToken, progress, cancellationToken), cancellationToken) + .OnSuccess(t => + { + state = t.Result; + }); + } + + internal Task SaveExternal() + { + Dictionary strs = new Dictionary() + { + { "url", this.Url.ToString() }, + { "name",this.Name }, + { "mime_type",this.MimeType}, + { "metaData",this.MetaData} + }; + AVCommand cmd = null; + + if (!string.IsNullOrEmpty(this.ObjectId)) + { + cmd = new AVCommand("files/" + this.ObjectId, + method: "PUT", sessionToken: AVUser.CurrentSessionToken, data: strs); + } + else + { + cmd = new AVCommand("files", method: "POST", sessionToken: AVUser.CurrentSessionToken, data: strs); + } + + return AVPlugins.Instance.CommandRunner.RunCommandAsync(cmd).ContinueWith(t => + { + var result = t.Result.Item2; + this.state.ObjectId = result["objectId"].ToString(); + return AVClient.IsSuccessStatusCode(t.Result.Item1); + }); + } + + #endregion + + #region Compatible + private readonly static Dictionary MIMETypesDictionary; + private object mutex = new object(); + private bool isExternal; + /// + /// 文件在 LeanCloud 的唯一Id 标识 + /// + public string ObjectId + { + get + { + String str; + lock (this.mutex) + { + str = state.ObjectId; + } + return str; + } + } + + /// + /// 文件的元数据。 + /// + public IDictionary MetaData + { + get + { + return state.MetaData; + } + } + + /// + /// 文件是否为外链文件。 + /// + /// + /// + public bool IsExternal + { + get + { + return isExternal; + } + } + + static AVFile() + { + Dictionary strs = new Dictionary() + { + { "ai", "application/postscript" }, + { "aif", "audio/x-aiff" }, + { "aifc", "audio/x-aiff" }, + { "aiff", "audio/x-aiff" }, + { "asc", "text/plain" }, + { "atom", "application/atom+xml" }, + { "au", "audio/basic" }, + { "avi", "video/x-msvideo" }, + { "bcpio", "application/x-bcpio" }, + { "bin", "application/octet-stream" }, + { "bmp", "image/bmp" }, + { "cdf", "application/x-netcdf" }, + { "cgm", "image/cgm" }, + { "class", "application/octet-stream" }, + { "cpio", "application/x-cpio" }, + { "cpt", "application/mac-compactpro" }, + { "csh", "application/x-csh" }, + { "css", "text/css" }, + { "dcr", "application/x-director" }, + { "dif", "video/x-dv" }, + { "dir", "application/x-director" }, + { "djv", "image/vnd.djvu" }, + { "djvu", "image/vnd.djvu" }, + { "dll", "application/octet-stream" }, + { "dmg", "application/octet-stream" }, + { "dms", "application/octet-stream" }, + { "doc", "application/msword" }, + { "docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document" }, + { "dotx", "application/vnd.openxmlformats-officedocument.wordprocessingml.template" }, + { "docm", "application/vnd.ms-word.document.macroEnabled.12" }, + { "dotm", "application/vnd.ms-word.template.macroEnabled.12" }, + { "dtd", "application/xml-dtd" }, + { "dv", "video/x-dv" }, + { "dvi", "application/x-dvi" }, + { "dxr", "application/x-director" }, + { "eps", "application/postscript" }, + { "etx", "text/x-setext" }, + { "exe", "application/octet-stream" }, + { "ez", "application/andrew-inset" }, + { "gif", "image/gif" }, + { "gram", "application/srgs" }, + { "grxml", "application/srgs+xml" }, + { "gtar", "application/x-gtar" }, + { "hdf", "application/x-hdf" }, + { "hqx", "application/mac-binhex40" }, + { "htm", "text/html" }, + { "html", "text/html" }, + { "ice", "x-conference/x-cooltalk" }, + { "ico", "image/x-icon" }, + { "ics", "text/calendar" }, + { "ief", "image/ief" }, + { "ifb", "text/calendar" }, + { "iges", "model/iges" }, + { "igs", "model/iges" }, + { "jnlp", "application/x-java-jnlp-file" }, + { "jp2", "image/jp2" }, + { "jpe", "image/jpeg" }, + { "jpeg", "image/jpeg" }, + { "jpg", "image/jpeg" }, + { "js", "application/x-javascript" }, + { "kar", "audio/midi" }, + { "latex", "application/x-latex" }, + { "lha", "application/octet-stream" }, + { "lzh", "application/octet-stream" }, + { "m3u", "audio/x-mpegurl" }, + { "m4a", "audio/mp4a-latm" }, + { "m4b", "audio/mp4a-latm" }, + { "m4p", "audio/mp4a-latm" }, + { "m4u", "video/vnd.mpegurl" }, + { "m4v", "video/x-m4v" }, + { "mac", "image/x-macpaint" }, + { "man", "application/x-troff-man" }, + { "mathml", "application/mathml+xml" }, + { "me", "application/x-troff-me" }, + { "mesh", "model/mesh" }, + { "mid", "audio/midi" }, + { "midi", "audio/midi" }, + { "mif", "application/vnd.mif" }, + { "mov", "video/quicktime" }, + { "movie", "video/x-sgi-movie" }, + { "mp2", "audio/mpeg" }, + { "mp3", "audio/mpeg" }, + { "mp4", "video/mp4" }, + { "mpe", "video/mpeg" }, + { "mpeg", "video/mpeg" }, + { "mpg", "video/mpeg" }, + { "mpga", "audio/mpeg" }, + { "ms", "application/x-troff-ms" }, + { "msh", "model/mesh" }, + { "mxu", "video/vnd.mpegurl" }, + { "nc", "application/x-netcdf" }, + { "oda", "application/oda" }, + { "ogg", "application/ogg" }, + { "pbm", "image/x-portable-bitmap" }, + { "pct", "image/pict" }, + { "pdb", "chemical/x-pdb" }, + { "pdf", "application/pdf" }, + { "pgm", "image/x-portable-graymap" }, + { "pgn", "application/x-chess-pgn" }, + { "pic", "image/pict" }, + { "pict", "image/pict" }, + { "png", "image/png" }, + { "pnm", "image/x-portable-anymap" }, + { "pnt", "image/x-macpaint" }, + { "pntg", "image/x-macpaint" }, + { "ppm", "image/x-portable-pixmap" }, + { "ppt", "application/vnd.ms-powerpoint" }, + { "pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation" }, + { "potx", "application/vnd.openxmlformats-officedocument.presentationml.template" }, + { "ppsx", "application/vnd.openxmlformats-officedocument.presentationml.slideshow" }, + { "ppam", "application/vnd.ms-powerpoint.addin.macroEnabled.12" }, + { "pptm", "application/vnd.ms-powerpoint.presentation.macroEnabled.12" }, + { "potm", "application/vnd.ms-powerpoint.template.macroEnabled.12" }, + { "ppsm", "application/vnd.ms-powerpoint.slideshow.macroEnabled.12" }, + { "ps", "application/postscript" }, + { "qt", "video/quicktime" }, + { "qti", "image/x-quicktime" }, + { "qtif", "image/x-quicktime" }, + { "ra", "audio/x-pn-realaudio" }, + { "ram", "audio/x-pn-realaudio" }, + { "ras", "image/x-cmu-raster" }, + { "rdf", "application/rdf+xml" }, + { "rgb", "image/x-rgb" }, + { "rm", "application/vnd.rn-realmedia" }, + { "roff", "application/x-troff" }, + { "rtf", "text/rtf" }, + { "rtx", "text/richtext" }, + { "sgm", "text/sgml" }, + { "sgml", "text/sgml" }, + { "sh", "application/x-sh" }, + { "shar", "application/x-shar" }, + { "silo", "model/mesh" }, + { "sit", "application/x-stuffit" }, + { "skd", "application/x-koan" }, + { "skm", "application/x-koan" }, + { "skp", "application/x-koan" }, + { "skt", "application/x-koan" }, + { "smi", "application/smil" }, + { "smil", "application/smil" }, + { "snd", "audio/basic" }, + { "so", "application/octet-stream" }, + { "spl", "application/x-futuresplash" }, + { "src", "application/x-wais-Source" }, + { "sv4cpio", "application/x-sv4cpio" }, + { "sv4crc", "application/x-sv4crc" }, + { "svg", "image/svg+xml" }, + { "swf", "application/x-shockwave-flash" }, + { "t", "application/x-troff" }, + { "tar", "application/x-tar" }, + { "tcl", "application/x-tcl" }, + { "tex", "application/x-tex" }, + { "texi", "application/x-texinfo" }, + { "texinfo", "application/x-texinfo" }, + { "tif", "image/tiff" }, + { "tiff", "image/tiff" }, + { "tr", "application/x-troff" }, + { "tsv", "text/tab-separated-values" }, + { "txt", "text/plain" }, + { "ustar", "application/x-ustar" }, + { "vcd", "application/x-cdlink" }, + { "vrml", "model/vrml" }, + { "vxml", "application/voicexml+xml" }, + { "wav", "audio/x-wav" }, + { "wbmp", "image/vnd.wap.wbmp" }, + { "wbmxl", "application/vnd.wap.wbxml" }, + { "wml", "text/vnd.wap.wml" }, + { "wmlc", "application/vnd.wap.wmlc" }, + { "wmls", "text/vnd.wap.wmlscript" }, + { "wmlsc", "application/vnd.wap.wmlscriptc" }, + { "wrl", "model/vrml" }, + { "xbm", "image/x-xbitmap" }, + { "xht", "application/xhtml+xml" }, + { "xhtml", "application/xhtml+xml" }, + { "xls", "application/vnd.ms-excel" }, + { "xml", "application/xml" }, + { "xpm", "image/x-xpixmap" }, + { "xsl", "application/xml" }, + { "xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" }, + { "xltx", "application/vnd.openxmlformats-officedocument.spreadsheetml.template" }, + { "xlsm", "application/vnd.ms-excel.sheet.macroEnabled.12" }, + { "xltm", "application/vnd.ms-excel.template.macroEnabled.12" }, + { "xlam", "application/vnd.ms-excel.addin.macroEnabled.12" }, + { "xlsb", "application/vnd.ms-excel.sheet.binary.macroEnabled.12" }, + { "xslt", "application/xslt+xml" }, + { "xul", "application/vnd.mozilla.xul+xml" }, + { "xwd", "image/x-xwindowdump" }, + { "xyz", "chemical/x-xyz" }, + { "zip", "application/zip" }, + }; + AVFile.MIMETypesDictionary = strs; + } + internal static string GetMIMEType(string fileName) + { + try + { + string str = Path.GetExtension(fileName).Remove(0, 1); + if (!AVFile.MIMETypesDictionary.ContainsKey(str)) + { + return "unknown/unknown"; + } + return AVFile.MIMETypesDictionary[str]; + } + catch + { + return "unknown/unknown"; + } + } + + /// + /// 根据 ObjectId 获取文件 + /// + /// 获取之后并没有实际执行下载,只是加载了文件的元信息以及物理地址(Url) + /// + public static Task GetFileWithObjectIdAsync(string objectId, CancellationToken cancellationToken) + { + string currentSessionToken = AVUser.CurrentSessionToken; + return FileController.GetAsync(objectId, currentSessionToken, cancellationToken).OnSuccess(_ => + { + var filestate = _.Result; + return new AVFile(filestate); + }); + } + + public static AVFile CreateWithoutData(string objectId) + { + return new AVFile(objectId); + } + + public static AVFile CreateWithState(FileState state) + { + return new AVFile(state); + } + + public static AVFile CreateWithData(string objectId,string name, string url,IDictionary metaData) + { + var fileState = new FileState(); + fileState.Name = name; + fileState.ObjectId = objectId; + fileState.Url = new Uri(url); + fileState.MetaData = metaData; + return CreateWithState(fileState); + } + /// + /// 根据 ObjectId 获取文件 + /// + /// 获取之后并没有实际执行下载,只是加载了文件的元信息以及物理地址(Url) + /// + public static Task GetFileWithObjectIdAsync(string objectId) + { + return GetFileWithObjectIdAsync(objectId, CancellationToken.None); + } + + internal void MergeFromJSON(IDictionary jsonData) + { + lock (this.mutex) + { + state.ObjectId = jsonData["objectId"] as string; + state.Url = new Uri(jsonData["url"] as string, UriKind.Absolute); + if (jsonData.ContainsKey("name")) + { + state.Name = jsonData["name"] as string; + } + if (jsonData.ContainsKey("metaData")) + { + state.MetaData = jsonData["metaData"] as Dictionary; + } + + } + } + + /// + /// 删除文件 + /// + /// Task + public Task DeleteAsync() + { + return DeleteAsync(CancellationToken.None); + } + internal Task DeleteAsync(CancellationToken cancellationToken) + { + return taskQueue.Enqueue(toAwait => DeleteAsync(toAwait, cancellationToken), + cancellationToken); + + } + internal Task DeleteAsync(Task toAwait, CancellationToken cancellationToken) + { + if (ObjectId == null) + { + return Task.FromResult(0); + } + + string sessionToken = AVUser.CurrentSessionToken; + + return toAwait.OnSuccess(_ => + { + return FileController.DeleteAsync(state, sessionToken, cancellationToken); + }).Unwrap().OnSuccess(_ => { }); + } + #endregion + } + + +} diff --git a/Storage/Source/Public/AVGeoDistance.cs b/Storage/Source/Public/AVGeoDistance.cs new file mode 100644 index 0000000..e761658 --- /dev/null +++ b/Storage/Source/Public/AVGeoDistance.cs @@ -0,0 +1,78 @@ +namespace LeanCloud +{ + /// + /// Represents a distance between two AVGeoPoints. + /// + public struct AVGeoDistance + { + private const double EarthMeanRadiusKilometers = 6371.0; + private const double EarthMeanRadiusMiles = 3958.8; + + /// + /// Creates a AVGeoDistance. + /// + /// The distance in radians. + public AVGeoDistance(double radians) + : this() + { + Radians = radians; + } + + /// + /// Gets the distance in radians. + /// + public double Radians { get; private set; } + + /// + /// Gets the distance in miles. + /// + public double Miles + { + get + { + return Radians * EarthMeanRadiusMiles; + } + } + + /// + /// Gets the distance in kilometers. + /// + public double Kilometers + { + get + { + return Radians * EarthMeanRadiusKilometers; + } + } + + /// + /// Gets a AVGeoDistance from a number of miles. + /// + /// The number of miles. + /// A AVGeoDistance for the given number of miles. + public static AVGeoDistance FromMiles(double miles) + { + return new AVGeoDistance(miles / EarthMeanRadiusMiles); + } + + /// + /// Gets a AVGeoDistance from a number of kilometers. + /// + /// The number of kilometers. + /// A AVGeoDistance for the given number of kilometers. + public static AVGeoDistance FromKilometers(double kilometers) + { + return new AVGeoDistance(kilometers / EarthMeanRadiusKilometers); + } + + /// + /// Gets a AVGeoDistance from a number of radians. + /// + /// The number of radians. + /// A AVGeoDistance for the given number of radians. + public static AVGeoDistance FromRadians(double radians) + { + return new AVGeoDistance(radians); + } + } +} diff --git a/Storage/Source/Public/AVGeoPoint.cs b/Storage/Source/Public/AVGeoPoint.cs new file mode 100644 index 0000000..7ca0f57 --- /dev/null +++ b/Storage/Source/Public/AVGeoPoint.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using LeanCloud.Storage.Internal; + +namespace LeanCloud +{ + /// + /// AVGeoPoint represents a latitude / longitude point that may be associated + /// with a key in a AVObject or used as a reference point for geo queries. + /// This allows proximity-based queries on the key. + /// + /// Only one key in a class may contain a GeoPoint. + /// + public struct AVGeoPoint : IJsonConvertible + { + /// + /// Constructs a AVGeoPoint with the specified latitude and longitude. + /// + /// The point's latitude. + /// The point's longitude. + public AVGeoPoint(double latitude, double longitude) + : this() + { + Latitude = latitude; + Longitude = longitude; + } + + private double latitude; + /// + /// Gets or sets the latitude of the GeoPoint. Valid range is [-90, 90]. + /// Extremes should not be used. + /// + public double Latitude + { + get + { + return latitude; + } + set + { + if (value > 90 || value < -90) + { + throw new ArgumentOutOfRangeException("value", + "Latitude must be within the range [-90, 90]"); + } + latitude = value; + } + } + + private double longitude; + /// + /// Gets or sets the longitude. Valid range is [-180, 180]. + /// Extremes should not be used. + /// + public double Longitude + { + get + { + return longitude; + } + set + { + if (value > 180 || value < -180) + { + throw new ArgumentOutOfRangeException("value", + "Longitude must be within the range [-180, 180]"); + } + longitude = value; + } + } + + /// + /// Get the distance in radians between this point and another GeoPoint. This is the smallest angular + /// distance between the two points. + /// + /// GeoPoint describing the other point being measured against. + /// The distance in between the two points. + public AVGeoDistance DistanceTo(AVGeoPoint point) + { + double d2r = Math.PI / 180; // radian conversion factor + double lat1rad = Latitude * d2r; + double long1rad = longitude * d2r; + double lat2rad = point.Latitude * d2r; + double long2rad = point.Longitude * d2r; + double deltaLat = lat1rad - lat2rad; + double deltaLong = long1rad - long2rad; + double sinDeltaLatDiv2 = Math.Sin(deltaLat / 2); + double sinDeltaLongDiv2 = Math.Sin(deltaLong / 2); + // Square of half the straight line chord distance between both points. + // [0.0, 1.0] + double a = sinDeltaLatDiv2 * sinDeltaLatDiv2 + + Math.Cos(lat1rad) * Math.Cos(lat2rad) * sinDeltaLongDiv2 * sinDeltaLongDiv2; + a = Math.Min(1.0, a); + return new AVGeoDistance(2 * Math.Asin(Math.Sqrt(a))); + } + + IDictionary IJsonConvertible.ToJSON() + { + return new Dictionary + { + { "__type", "GeoPoint" }, + { "latitude", Latitude }, + { "longitude", Longitude } + }; + } + } +} diff --git a/Storage/Source/Public/AVObject.cs b/Storage/Source/Public/AVObject.cs new file mode 100644 index 0000000..61ec98f --- /dev/null +++ b/Storage/Source/Public/AVObject.cs @@ -0,0 +1,2100 @@ +using LeanCloud.Storage.Internal; +using LeanCloud.Utilities; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Net; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using System.Collections; + +namespace LeanCloud +{ + /// + /// The AVObject is a local representation of data that can be saved and + /// retrieved from the LeanCloud cloud. + /// + /// + /// The basic workflow for creating new data is to construct a new AVObject, + /// use the indexer to fill it with data, and then use SaveAsync() to persist to the + /// database. + /// + /// + /// The basic workflow for accessing existing data is to use a AVQuery + /// to specify which existing data to retrieve. + /// + /// + public class AVObject : IEnumerable>, INotifyPropertyChanged, INotifyPropertyUpdated, INotifyCollectionPropertyUpdated, IAVObject + { + private static readonly string AutoClassName = "_Automatic"; + +#if UNITY + private static readonly bool isCompiledByIL2CPP = AppDomain.CurrentDomain.FriendlyName.Equals("IL2CPP Root Domain"); +#else + private static readonly bool isCompiledByIL2CPP = false; +#endif + + + internal readonly object mutex = new object(); + + private readonly LinkedList> operationSetQueue = + new LinkedList>(); + private readonly IDictionary estimatedData = new Dictionary(); + + private static readonly ThreadLocal isCreatingPointer = new ThreadLocal(() => false); + + private bool hasBeenFetched; + private bool dirty; + internal TaskQueue taskQueue = new TaskQueue(); + + private IObjectState state; + internal void MutateState(Action func) + { + lock (mutex) + { + state = state.MutatedClone(func); + + // Refresh the estimated data. + RebuildEstimatedData(); + } + } + + public IObjectState State + { + get + { + return state; + } + } + + internal static IAVObjectController ObjectController + { + get + { + return AVPlugins.Instance.ObjectController; + } + } + + internal static IObjectSubclassingController SubclassingController + { + get + { + return AVPlugins.Instance.SubclassingController; + } + } + + public static string GetSubClassName() + { + return SubclassingController.GetClassName(typeof(TAVObject)); + } + + #region AVObject Creation + + /// + /// Constructor for use in AVObject subclasses. Subclasses must specify a AVClassName attribute. + /// + protected AVObject() + : this(AutoClassName) + { + } + + /// + /// Constructs a new AVObject with no data in it. A AVObject constructed in this way will + /// not have an ObjectId and will not persist to the database until + /// is called. + /// + /// + /// Class names must be alphanumerical plus underscore, and start with a letter. It is recommended + /// to name classes in CamelCaseLikeThis. + /// + /// The className for this AVObject. + public AVObject(string className) + { + // We use a ThreadLocal rather than passing a parameter so that createWithoutData can do the + // right thing with subclasses. It's ugly and terrible, but it does provide the development + // experience we generally want, so... yeah. Sorry to whomever has to deal with this in the + // future. I pinky-swear we won't make a habit of this -- you believe me, don't you? + var isPointer = isCreatingPointer.Value; + isCreatingPointer.Value = false; + + if (className == null) + { + throw new ArgumentException("You must specify a LeanCloud class name when creating a new AVObject."); + } + if (AutoClassName.Equals(className)) + { + className = SubclassingController.GetClassName(GetType()); + } + // If this is supposed to be created by a factory but wasn't, throw an exception + if (!SubclassingController.IsTypeValid(className, GetType())) + { + throw new ArgumentException( + "You must create this type of AVObject using AVObject.Create() or the proper subclass."); + } + state = new MutableObjectState + { + ClassName = className + }; + OnPropertyChanged("ClassName"); + + operationSetQueue.AddLast(new Dictionary()); + if (!isPointer) + { + hasBeenFetched = true; + IsDirty = true; + SetDefaultValues(); + } + else + { + IsDirty = false; + hasBeenFetched = false; + } + } + + /// + /// Creates a new AVObject based upon a class name. If the class name is a special type (e.g. + /// for ), then the appropriate type of AVObject is returned. + /// + /// The class of object to create. + /// A new AVObject for the given class name. + public static AVObject Create(string className) + { + return SubclassingController.Instantiate(className); + } + + /// + /// Creates a reference to an existing AVObject for use in creating associations between + /// AVObjects. Calling on this object will return + /// false until has been called. + /// No network request will be made. + /// + /// The object's class. + /// The object id for the referenced object. + /// A AVObject without data. + public static AVObject CreateWithoutData(string className, string objectId) + { + isCreatingPointer.Value = true; + try + { + var result = SubclassingController.Instantiate(className); + result.ObjectId = objectId; + result.IsDirty = false; + if (result.IsDirty) + { + throw new InvalidOperationException( + "A AVObject subclass default constructor must not make changes to the object that cause it to be dirty."); + } + return result; + } + finally + { + isCreatingPointer.Value = false; + } + } + + /// + /// Creates a new AVObject based upon a given subclass type. + /// + /// A new AVObject for the given class name. + public static T Create() where T : AVObject + { + return (T)SubclassingController.Instantiate(SubclassingController.GetClassName(typeof(T))); + } + + /// + /// Creates a reference to an existing AVObject for use in creating associations between + /// AVObjects. Calling on this object will return + /// false until has been called. + /// No network request will be made. + /// + /// The object id for the referenced object. + /// A AVObject without data. + public static T CreateWithoutData(string objectId) where T : AVObject + { + return (T)CreateWithoutData(SubclassingController.GetClassName(typeof(T)), objectId); + } + + /// + /// restore a AVObject of subclass instance from IObjectState. + /// + /// IObjectState after encode from Dictionary. + /// The name of the subclass. + public static T FromState(IObjectState state, string defaultClassName) where T : AVObject + { + string className = state.ClassName ?? defaultClassName; + + T obj = (T)CreateWithoutData(className, state.ObjectId); + obj.HandleFetchResult(state); + + return obj; + } + + #endregion + + public static IDictionary GetPropertyMappings(string className) + { + return SubclassingController.GetPropertyMappings(className); + } + + private static string GetFieldForPropertyName(string className, string propertyName) + { + String fieldName = null; + SubclassingController.GetPropertyMappings(className).TryGetValue(propertyName, out fieldName); + return fieldName; + } + + /// + /// Sets the value of a property based upon its associated AVFieldName attribute. + /// + /// The new value. + /// The name of the property. + /// The type for the property. + protected virtual void SetProperty(T value, +#if !UNITY + [CallerMemberName] string propertyName = null +#else + string propertyName +#endif +) + { + this[GetFieldForPropertyName(ClassName, propertyName)] = value; + } + + /// + /// Gets a relation for a property based upon its associated AVFieldName attribute. + /// + /// The AVRelation for the property. + /// The name of the property. + /// The AVObject subclass type of the AVRelation. + protected AVRelation GetRelationProperty( +#if !UNITY +[CallerMemberName] string propertyName = null +#else +string propertyName +#endif +) where T : AVObject + { + return GetRelation(GetFieldForPropertyName(ClassName, propertyName)); + } + + /// + /// Gets the value of a property based upon its associated AVFieldName attribute. + /// + /// The value of the property. + /// The name of the property. + /// The return type of the property. + protected virtual T GetProperty( +#if !UNITY +[CallerMemberName] string propertyName = null +#else +string propertyName +#endif +) + { + return GetProperty(default(T), propertyName); + } + + /// + /// Gets the value of a property based upon its associated AVFieldName attribute. + /// + /// The value of the property. + /// The value to return if the property is not present on the AVObject. + /// The name of the property. + /// The return type of the property. + protected virtual T GetProperty(T defaultValue, +#if !UNITY + [CallerMemberName] string propertyName = null +#else + string propertyName +#endif +) + { + T result; + if (TryGetValue(GetFieldForPropertyName(ClassName, propertyName), out result)) + { + return result; + } + return defaultValue; + } + + /// + /// Allows subclasses to set values for non-pointer construction. + /// + internal virtual void SetDefaultValues() + { + } + + /// + /// Registers a custom subclass type with the LeanCloud SDK, enabling strong-typing of those AVObjects whenever + /// they appear. Subclasses must specify the AVClassName attribute, have a default constructor, and properties + /// backed by AVObject fields should have AVFieldName attributes supplied. + /// + /// The AVObject subclass type to register. + public static void RegisterSubclass() where T : AVObject, new() + { + SubclassingController.RegisterSubclass(typeof(T)); + } + + internal static void UnregisterSubclass() where T : AVObject, new() + { + SubclassingController.UnregisterSubclass(typeof(T)); + } + + /// + /// Clears any changes to this object made since the last call to . + /// + public void Revert() + { + lock (mutex) + { + bool wasDirty = CurrentOperations.Count > 0; + if (wasDirty) + { + CurrentOperations.Clear(); + RebuildEstimatedData(); + OnPropertyChanged("IsDirty"); + } + } + } + + internal virtual void HandleFetchResult(IObjectState serverState) + { + lock (mutex) + { + MergeFromServer(serverState); + } + } + + internal void HandleFailedSave( + IDictionary operationsBeforeSave) + { + lock (mutex) + { + var opNode = operationSetQueue.Find(operationsBeforeSave); + var nextOperations = opNode.Next.Value; + bool wasDirty = nextOperations.Count > 0; + operationSetQueue.Remove(opNode); + // Merge the data from the failed save into the next save. + foreach (var pair in operationsBeforeSave) + { + var operation1 = pair.Value; + IAVFieldOperation operation2 = null; + nextOperations.TryGetValue(pair.Key, out operation2); + if (operation2 != null) + { + operation2 = operation2.MergeWithPrevious(operation1); + } + else + { + operation2 = operation1; + } + nextOperations[pair.Key] = operation2; + } + if (!wasDirty && nextOperations == CurrentOperations && operationsBeforeSave.Count > 0) + { + OnPropertyChanged("IsDirty"); + } + } + } + + internal virtual void HandleSave(IObjectState serverState) + { + lock (mutex) + { + var operationsBeforeSave = operationSetQueue.First.Value; + operationSetQueue.RemoveFirst(); + + // Merge the data from the save and the data from the server into serverData. + //MutateState(mutableClone => + //{ + // mutableClone.Apply(operationsBeforeSave); + //}); + state = state.MutatedClone((objectState) => objectState.Apply(operationsBeforeSave)); + MergeFromServer(serverState); + } + } + + public virtual void MergeFromServer(IObjectState serverState) + { + // Make a new serverData with fetched values. + var newServerData = serverState.ToDictionary(t => t.Key, t => t.Value); + + lock (mutex) + { + // Trigger handler based on serverState + if (serverState.ObjectId != null) + { + // If the objectId is being merged in, consider this object to be fetched. + hasBeenFetched = true; + OnPropertyChanged("IsDataAvailable"); + } + + if (serverState.UpdatedAt != null) + { + OnPropertyChanged("UpdatedAt"); + } + + if (serverState.CreatedAt != null) + { + OnPropertyChanged("CreatedAt"); + } + + // We cache the fetched object because subsequent Save operation might flush + // the fetched objects into Pointers. + IDictionary fetchedObject = CollectFetchedObjects(); + + foreach (var pair in serverState) + { + var value = pair.Value; + if (value is AVObject) + { + // Resolve fetched object. + var avObject = value as AVObject; + if (fetchedObject.ContainsKey(avObject.ObjectId)) + { + value = fetchedObject[avObject.ObjectId]; + } + } + newServerData[pair.Key] = value; + } + + IsDirty = false; + serverState = serverState.MutatedClone(mutableClone => + { + mutableClone.ServerData = newServerData; + }); + MutateState(mutableClone => + { + mutableClone.Apply(serverState); + }); + } + } + + internal void MergeFromObject(AVObject other) + { + lock (mutex) + { + // If they point to the same instance, we don't need to merge + if (this == other) + { + return; + } + } + + // Clear out any changes on this object. + if (operationSetQueue.Count != 1) + { + throw new InvalidOperationException("Attempt to MergeFromObject during save."); + } + operationSetQueue.Clear(); + foreach (var operationSet in other.operationSetQueue) + { + operationSetQueue.AddLast(operationSet.ToDictionary(entry => entry.Key, + entry => entry.Value)); + } + + lock (mutex) + { + state = other.State; + } + RebuildEstimatedData(); + } + + private bool HasDirtyChildren + { + get + { + lock (mutex) + { + return FindUnsavedChildren().FirstOrDefault() != null; + } + } + } + + /// + /// Flattens dictionaries and lists into a single enumerable of all contained objects + /// that can then be queried over. + /// + /// The root of the traversal + /// Whether to traverse into AVObjects' children + /// Whether to include the root in the result + /// + internal static IEnumerable DeepTraversal( + object root, bool traverseAVObjects = false, bool yieldRoot = false) + { + var items = DeepTraversalInternal(root, + traverseAVObjects, + new HashSet(new IdentityEqualityComparer())); + if (yieldRoot) + { + return new[] { root }.Concat(items); + } + else + { + return items; + } + } + + private static IEnumerable DeepTraversalInternal( + object root, bool traverseAVObjects, ICollection seen) + { + seen.Add(root); + var itemsToVisit = isCompiledByIL2CPP ? (System.Collections.IEnumerable)null : (IEnumerable)null; + var dict = Conversion.As>(root); + if (dict != null) + { + itemsToVisit = dict.Values; + } + else + { + var list = Conversion.As>(root); + if (list != null) + { + itemsToVisit = list; + } + else if (traverseAVObjects) + { + var obj = root as AVObject; + if (obj != null) + { + itemsToVisit = obj.Keys.ToList().Select(k => obj[k]); + } + } + } + if (itemsToVisit != null) + { + foreach (var i in itemsToVisit) + { + if (!seen.Contains(i)) + { + yield return i; + var children = DeepTraversalInternal(i, traverseAVObjects, seen); + foreach (var child in children) + { + yield return child; + } + } + } + } + } + + private IEnumerable FindUnsavedChildren() + { + return DeepTraversal(estimatedData) + .OfType() + .Where(o => o.IsDirty); + } + + /// + /// Deep traversal of this object to grab a copy of any object referenced by this object. + /// These instances may have already been fetched, and we don't want to lose their data when + /// refreshing or saving. + /// + /// Map of objectId to AVObject which have been fetched. + private IDictionary CollectFetchedObjects() + { + return DeepTraversal(estimatedData) + .OfType() + .Where(o => o.ObjectId != null && o.IsDataAvailable) + .GroupBy(o => o.ObjectId) + .ToDictionary(group => group.Key, group => group.Last()); + } + + public static IDictionary ToJSONObjectForSaving( + IDictionary operations) + { + var result = new Dictionary(); + foreach (var pair in operations) + { + // AVRPCSerialize the data + var operation = pair.Value; + + result[pair.Key] = PointerOrLocalIdEncoder.Instance.Encode(operation); + } + return result; + } + + internal IDictionary EncodeForSaving(IDictionary data) + { + var result = new Dictionary(); + lock (this.mutex) + { + foreach (var key in data.Keys) + { + var value = data[key]; + result.Add(key, PointerOrLocalIdEncoder.Instance.Encode(value)); + } + } + + return result; + } + + + internal IDictionary ServerDataToJSONObjectForSerialization() + { + return PointerOrLocalIdEncoder.Instance.Encode(state.ToDictionary(t => t.Key, t => t.Value)) + as IDictionary; + } + + #region Save Object(s) + + /// + /// Pushes new operations onto the queue and returns the current set of operations. + /// + public IDictionary StartSave() + { + lock (mutex) + { + var currentOperations = CurrentOperations; + operationSetQueue.AddLast(new Dictionary()); + OnPropertyChanged("IsDirty"); + return currentOperations; + } + } + + protected virtual Task SaveAsync(Task toAwait, + CancellationToken cancellationToken) + { + IDictionary currentOperations = null; + if (!IsDirty) + { + return Task.FromResult(0); + } + + Task deepSaveTask; + string sessionToken; + lock (mutex) + { + // Get the JSON representation of the object. + currentOperations = StartSave(); + + sessionToken = AVUser.CurrentSessionToken; + + deepSaveTask = DeepSaveAsync(estimatedData, sessionToken, cancellationToken); + } + + return deepSaveTask.OnSuccess(_ => + { + return toAwait; + }).Unwrap().OnSuccess(_ => + { + return ObjectController.SaveAsync(state, + currentOperations, + sessionToken, + cancellationToken); + }).Unwrap().ContinueWith(t => + { + if (t.IsFaulted || t.IsCanceled) + { + HandleFailedSave(currentOperations); + } + else + { + var serverState = t.Result; + HandleSave(serverState); + } + return t; + }).Unwrap(); + } + + /// + /// Saves this object to the server. + /// + public virtual Task SaveAsync() + { + return SaveAsync(CancellationToken.None); + } + + /// + /// Saves this object to the server. + /// + /// The cancellation token. + public virtual Task SaveAsync(CancellationToken cancellationToken) + { + return taskQueue.Enqueue(toAwait => SaveAsync(toAwait, cancellationToken), + cancellationToken); + } + + internal virtual Task FetchAsyncInternal( + Task toAwait, + IDictionary queryString, + CancellationToken cancellationToken) + { + return toAwait.OnSuccess(_ => + { + if (ObjectId == null) + { + throw new InvalidOperationException("Cannot refresh an object that hasn't been saved to the server."); + } + if (queryString == null) + { + queryString = new Dictionary(); + } + + return ObjectController.FetchAsync(state, queryString, AVUser.CurrentSessionToken, cancellationToken); + }).Unwrap().OnSuccess(t => + { + HandleFetchResult(t.Result); + return this; + }); + } + + private static Task DeepSaveAsync(object obj, string sessionToken, CancellationToken cancellationToken) + { + var objects = new List(); + CollectDirtyChildren(obj, objects); + + var uniqueObjects = new HashSet(objects, + new IdentityEqualityComparer()); + + var saveDirtyFileTasks = DeepTraversal(obj, true) + .OfType() + .Where(f => f.IsDirty) + .Select(f => f.SaveAsync(cancellationToken)).ToList(); + + return Task.WhenAll(saveDirtyFileTasks).OnSuccess(_ => + { + IEnumerable remaining = new List(uniqueObjects); + return InternalExtensions.WhileAsync(() => Task.FromResult(remaining.Any()), () => + { + // Partition the objects into two sets: those that can be saved immediately, + // and those that rely on other objects to be created first. + var current = (from item in remaining + where item.CanBeSerialized + select item).ToList(); + var nextBatch = (from item in remaining + where !item.CanBeSerialized + select item).ToList(); + remaining = nextBatch; + + if (current.Count == 0) + { + // We do cycle-detection when building the list of objects passed to this + // function, so this should never get called. But we should check for it + // anyway, so that we get an exception instead of an infinite loop. + throw new InvalidOperationException( + "Unable to save a AVObject with a relation to a cycle."); + } + + // Save all of the objects in current. + return AVObject.EnqueueForAll(current, toAwait => + { + return toAwait.OnSuccess(__ => + { + var states = (from item in current + select item.state).ToList(); + var operationsList = (from item in current + select item.StartSave()).ToList(); + + var saveTasks = ObjectController.SaveAllAsync(states, + operationsList, + sessionToken, + cancellationToken); + + return Task.WhenAll(saveTasks).ContinueWith(t => + { + if (t.IsFaulted || t.IsCanceled) + { + foreach (var pair in current.Zip(operationsList, (item, ops) => new { item, ops })) + { + pair.item.HandleFailedSave(pair.ops); + } + } + else + { + var serverStates = t.Result; + foreach (var pair in current.Zip(serverStates, (item, state) => new { item, state })) + { + pair.item.HandleSave(pair.state); + } + } + cancellationToken.ThrowIfCancellationRequested(); + return t; + }).Unwrap(); + }).Unwrap().OnSuccess(t => (object)null); + }, cancellationToken); + }); + }).Unwrap(); + } + + /// + /// Saves each object in the provided list. + /// + /// The objects to save. + public static Task SaveAllAsync(IEnumerable objects) where T : AVObject + { + return SaveAllAsync(objects, CancellationToken.None); + } + + /// + /// Saves each object in the provided list. + /// + /// The objects to save. + /// The cancellation token. + public static Task SaveAllAsync( + IEnumerable objects, CancellationToken cancellationToken) where T : AVObject + { + return DeepSaveAsync(objects.ToList(), AVUser.CurrentSessionToken, cancellationToken); + } + + #endregion + + #region Fetch Object(s) + + /// + /// Fetches this object with the data from the server. + /// + /// The cancellation token. + internal Task FetchAsyncInternal(CancellationToken cancellationToken) + { + return FetchAsyncInternal(null, cancellationToken); + } + + internal Task FetchAsyncInternal(IDictionary queryString, CancellationToken cancellationToken) + { + return taskQueue.Enqueue(toAwait => FetchAsyncInternal(toAwait, queryString, cancellationToken), + cancellationToken); + } + + internal Task FetchIfNeededAsyncInternal( + Task toAwait, CancellationToken cancellationToken) + { + if (!IsDataAvailable) + { + return FetchAsyncInternal(toAwait, null, cancellationToken); + } + return Task.FromResult(this); + } + + /// + /// If this AVObject has not been fetched (i.e. returns + /// false), fetches this object with the data from the server. + /// + /// The cancellation token. + internal Task FetchIfNeededAsyncInternal(CancellationToken cancellationToken) + { + return taskQueue.Enqueue(toAwait => FetchIfNeededAsyncInternal(toAwait, cancellationToken), + cancellationToken); + } + + /// + /// Fetches all of the objects that don't have data in the provided list. + /// + /// The list passed in for convenience. + public static Task> FetchAllIfNeededAsync( + IEnumerable objects) where T : AVObject + { + return FetchAllIfNeededAsync(objects, CancellationToken.None); + } + + /// + /// Fetches all of the objects that don't have data in the provided list. + /// + /// The objects to fetch. + /// The cancellation token. + /// The list passed in for convenience. + public static Task> FetchAllIfNeededAsync( + IEnumerable objects, CancellationToken cancellationToken) where T : AVObject + { + return AVObject.EnqueueForAll(objects.Cast(), (Task toAwait) => + { + return FetchAllInternalAsync(objects, false, toAwait, cancellationToken); + }, cancellationToken); + } + + /// + /// Fetches all of the objects in the provided list. + /// + /// The objects to fetch. + /// The list passed in for convenience. + public static Task> FetchAllAsync( + IEnumerable objects) where T : AVObject + { + return FetchAllAsync(objects, CancellationToken.None); + } + + /// + /// Fetches all of the objects in the provided list. + /// + /// The objects to fetch. + /// The cancellation token. + /// The list passed in for convenience. + public static Task> FetchAllAsync( + IEnumerable objects, CancellationToken cancellationToken) where T : AVObject + { + return AVObject.EnqueueForAll(objects.Cast(), (Task toAwait) => + { + return FetchAllInternalAsync(objects, true, toAwait, cancellationToken); + }, cancellationToken); + } + + /// + /// Fetches all of the objects in the list. + /// + /// The objects to fetch. + /// If false, only objects without data will be fetched. + /// A task to await before starting. + /// The cancellation token. + /// The list passed in for convenience. + private static Task> FetchAllInternalAsync( + IEnumerable objects, bool force, Task toAwait, CancellationToken cancellationToken) where T : AVObject + { + return toAwait.OnSuccess(_ => + { + if (objects.Any(obj => { return obj.state.ObjectId == null; })) + { + throw new InvalidOperationException("You cannot fetch objects that haven't already been saved."); + } + + var objectsToFetch = (from obj in objects + where force || !obj.IsDataAvailable + select obj).ToList(); + + if (objectsToFetch.Count == 0) + { + return Task.FromResult(objects); + } + + // Do one Find for each class. + var findsByClass = + (from obj in objectsToFetch + group obj.ObjectId by obj.ClassName into classGroup + where classGroup.Count() > 0 + select new + { + ClassName = classGroup.Key, + FindTask = new AVQuery(classGroup.Key) + .WhereContainedIn("objectId", classGroup) + .FindAsync(cancellationToken) + }).ToDictionary(pair => pair.ClassName, pair => pair.FindTask); + + // Wait for all the Finds to complete. + return Task.WhenAll(findsByClass.Values.ToList()).OnSuccess(__ => + { + if (cancellationToken.IsCancellationRequested) + { + return objects; + } + + // Merge the data from the Finds into the input objects. + var pairs = from obj in objectsToFetch + from result in findsByClass[obj.ClassName].Result + where result.ObjectId == obj.ObjectId + select new { obj, result }; + foreach (var pair in pairs) + { + pair.obj.MergeFromObject(pair.result); + pair.obj.hasBeenFetched = true; + } + + return objects; + }); + }).Unwrap(); + } + + #endregion + + #region Delete Object + + internal Task DeleteAsync(Task toAwait, CancellationToken cancellationToken) + { + if (ObjectId == null) + { + return Task.FromResult(0); + } + + string sessionToken = AVUser.CurrentSessionToken; + + return toAwait.OnSuccess(_ => + { + return ObjectController.DeleteAsync(State, sessionToken, cancellationToken); + }).Unwrap().OnSuccess(_ => IsDirty = true); + } + + /// + /// Deletes this object on the server. + /// + public Task DeleteAsync() + { + return DeleteAsync(CancellationToken.None); + } + + /// + /// Deletes this object on the server. + /// + /// The cancellation token. + public Task DeleteAsync(CancellationToken cancellationToken) + { + return taskQueue.Enqueue(toAwait => DeleteAsync(toAwait, cancellationToken), + cancellationToken); + } + + /// + /// Deletes each object in the provided list. + /// + /// The objects to delete. + public static Task DeleteAllAsync(IEnumerable objects) where T : AVObject + { + return DeleteAllAsync(objects, CancellationToken.None); + } + + /// + /// Deletes each object in the provided list. + /// + /// The objects to delete. + /// The cancellation token. + public static Task DeleteAllAsync( + IEnumerable objects, CancellationToken cancellationToken) where T : AVObject + { + var uniqueObjects = new HashSet(objects.OfType().ToList(), + new IdentityEqualityComparer()); + + return AVObject.EnqueueForAll(uniqueObjects, toAwait => + { + var states = uniqueObjects.Select(t => t.state).ToList(); + return toAwait.OnSuccess(_ => + { + var deleteTasks = ObjectController.DeleteAllAsync(states, + AVUser.CurrentSessionToken, + cancellationToken); + + return Task.WhenAll(deleteTasks); + }).Unwrap().OnSuccess(t => + { + // Dirty all objects in memory. + foreach (var obj in uniqueObjects) + { + obj.IsDirty = true; + } + + return (object)null; + }); + }, cancellationToken); + } + + #endregion + + private static void CollectDirtyChildren(object node, + IList dirtyChildren, + ICollection seen, + ICollection seenNew) + { + foreach (var obj in DeepTraversal(node).OfType()) + { + ICollection scopedSeenNew; + // Check for cycles of new objects. Any such cycle means it will be impossible to save + // this collection of objects, so throw an exception. + if (obj.ObjectId != null) + { + scopedSeenNew = new HashSet(new IdentityEqualityComparer()); + } + else + { + if (seenNew.Contains(obj)) + { + throw new InvalidOperationException("Found a circular dependency while saving"); + } + scopedSeenNew = new HashSet(seenNew, new IdentityEqualityComparer()); + scopedSeenNew.Add(obj); + } + + // Check for cycles of any object. If this occurs, then there's no problem, but + // we shouldn't recurse any deeper, because it would be an infinite recursion. + if (seen.Contains(obj)) + { + return; + } + seen.Add(obj); + + // Recurse into this object's children looking for dirty children. + // We only need to look at the child object's current estimated data, + // because that's the only data that might need to be saved now. + CollectDirtyChildren(obj.estimatedData, dirtyChildren, seen, scopedSeenNew); + + if (obj.CheckIsDirty(false)) + { + dirtyChildren.Add(obj); + } + } + } + + /// + /// Helper version of CollectDirtyChildren so that callers don't have to add the internally + /// used parameters. + /// + private static void CollectDirtyChildren(object node, IList dirtyChildren) + { + CollectDirtyChildren(node, + dirtyChildren, + new HashSet(new IdentityEqualityComparer()), + new HashSet(new IdentityEqualityComparer())); + } + + /// + /// Returns true if the given object can be serialized for saving as a value + /// that is pointed to by a AVObject. + /// + private static bool CanBeSerializedAsValue(object value) + { + return DeepTraversal(value, yieldRoot: true) + .OfType() + .All(o => o.ObjectId != null); + } + + private bool CanBeSerialized + { + get + { + // This method is only used for batching sets of objects for saveAll + // and when saving children automatically. Since it's only used to + // determine whether or not save should be called on them, it only + // needs to examine their current values, so we use estimatedData. + lock (mutex) + { + return CanBeSerializedAsValue(estimatedData); + } + } + } + + /// + /// Adds a task to the queue for all of the given objects. + /// + private static Task EnqueueForAll(IEnumerable objects, + Func> taskStart, CancellationToken cancellationToken) + { + // The task that will be complete when all of the child queues indicate they're ready to start. + var readyToStart = new TaskCompletionSource(); + + // First, we need to lock the mutex for the queue for every object. We have to hold this + // from at least when taskStart() is called to when obj.taskQueue enqueue is called, so + // that saves actually get executed in the order they were setup by taskStart(). + // The locks have to be sorted so that we always acquire them in the same order. + // Otherwise, there's some risk of deadlock. + var lockSet = new LockSet(objects.Select(o => o.taskQueue.Mutex)); + + lockSet.Enter(); + try + { + + // The task produced by taskStart. By running this immediately, we allow everything prior + // to toAwait to run before waiting for all of the queues on all of the objects. + Task fullTask = taskStart(readyToStart.Task); + + // Add fullTask to each of the objects' queues. + var childTasks = new List(); + foreach (AVObject obj in objects) + { + obj.taskQueue.Enqueue((Task task) => + { + childTasks.Add(task); + return fullTask; + }, cancellationToken); + } + + // When all of the objects' queues are ready, signal fullTask that it's ready to go on. + Task.WhenAll(childTasks.ToArray()).ContinueWith((Task task) => + { + readyToStart.SetResult(null); + }); + + return fullTask; + } + finally + { + lockSet.Exit(); + } + } + + /// + /// Removes a key from the object's data if it exists. + /// + /// The key to remove. + public virtual void Remove(string key) + { + lock (mutex) + { + CheckKeyIsMutable(key); + + PerformOperation(key, AVDeleteOperation.Instance); + } + } + + + private IEnumerable ApplyOperations(IDictionary operations, + IDictionary map) + { + List appliedKeys = new List(); + lock (mutex) + { + foreach (var pair in operations) + { + object oldValue; + map.TryGetValue(pair.Key, out oldValue); + var newValue = pair.Value.Apply(oldValue, pair.Key); + if (newValue != AVDeleteOperation.DeleteToken) + { + map[pair.Key] = newValue; + } + else + { + map.Remove(pair.Key); + } + appliedKeys.Add(pair.Key); + } + } + return appliedKeys; + } + + /// + /// Regenerates the estimatedData map from the serverData and operations. + /// + internal void RebuildEstimatedData() + { + IEnumerable changedKeys = null; + + lock (mutex) + { + //estimatedData.Clear(); + List converdKeys = new List(); + foreach (var item in state) + { + var key = item.Key; + var value = item.Value; + if (!estimatedData.ContainsKey(key)) + { + converdKeys.Add(key); + } + else + { + var oldValue = estimatedData[key]; + if (oldValue != value) + { + converdKeys.Add(key); + } + estimatedData.Remove(key); + } + estimatedData.Add(item); + } + changedKeys = converdKeys; + foreach (var operations in operationSetQueue) + { + var appliedKeys = ApplyOperations(operations, estimatedData); + changedKeys = converdKeys.Concat(appliedKeys); + } + // We've just applied a bunch of operations to estimatedData which + // may have changed all of its keys. Notify of all keys and properties + // mapped to keys being changed. + OnFieldsChanged(changedKeys); + } + } + + /// + /// PerformOperation is like setting a value at an index, but instead of + /// just taking a new value, it takes a AVFieldOperation that modifies the value. + /// + internal void PerformOperation(string key, IAVFieldOperation operation) + { + lock (mutex) + { + var ifDirtyBeforePerform = this.IsDirty; + object oldValue; + estimatedData.TryGetValue(key, out oldValue); + object newValue = operation.Apply(oldValue, key); + if (newValue != AVDeleteOperation.DeleteToken) + { + estimatedData[key] = newValue; + } + else + { + estimatedData.Remove(key); + } + + IAVFieldOperation oldOperation; + bool wasDirty = CurrentOperations.Count > 0; + CurrentOperations.TryGetValue(key, out oldOperation); + var newOperation = operation.MergeWithPrevious(oldOperation); + CurrentOperations[key] = newOperation; + if (!wasDirty) + { + OnPropertyChanged("IsDirty"); + if (ifDirtyBeforePerform != wasDirty) + { + OnPropertyUpdated("IsDirty", ifDirtyBeforePerform, wasDirty); + } + } + + OnFieldsChanged(new[] { key }); + OnPropertyUpdated(key, oldValue, newValue); + } + } + + /// + /// Override to run validations on key/value pairs. Make sure to still + /// call the base version. + /// + internal virtual void OnSettingValue(ref string key, ref object value) + { + if (key == null) + { + throw new ArgumentNullException("key"); + } + } + + /// + /// Gets or sets a value on the object. It is recommended to name + /// keys in partialCamelCaseLikeThis. + /// + /// The key for the object. Keys must be alphanumeric plus underscore + /// and start with a letter. + /// The property is + /// retrieved and is not found. + /// The value for the key. + virtual public object this[string key] + { + get + { + lock (mutex) + { + CheckGetAccess(key); + + var value = estimatedData[key]; + + // A relation may be deserialized without a parent or key. Either way, + // make sure it's consistent. + var relation = value as AVRelationBase; + if (relation != null) + { + relation.EnsureParentAndKey(this, key); + } + + return value; + } + } + set + { + lock (mutex) + { + CheckKeyIsMutable(key); + + Set(key, value); + } + } + } + + /// + /// Perform Set internally which is not gated by mutability check. + /// + /// key for the object. + /// the value for the key. + internal void Set(string key, object value) + { + lock (mutex) + { + OnSettingValue(ref key, ref value); + + if (!AVEncoder.IsValidType(value)) + { + throw new ArgumentException("Invalid type for value: " + value.GetType().ToString()); + } + + PerformOperation(key, new AVSetOperation(value)); + } + } + + internal void SetIfDifferent(string key, T value) + { + T current; + bool hasCurrent = TryGetValue(key, out current); + if (value == null) + { + if (hasCurrent) + { + PerformOperation(key, AVDeleteOperation.Instance); + } + return; + } + if (!hasCurrent || !value.Equals(current)) + { + Set(key, value); + } + } + + #region Atomic Increment + + /// + /// Atomically increments the given key by 1. + /// + /// The key to increment. + public void Increment(string key) + { + Increment(key, 1); + } + + /// + /// Atomically increments the given key by the given number. + /// + /// The key to increment. + /// The amount to increment by. + public void Increment(string key, long amount) + { + lock (mutex) + { + CheckKeyIsMutable(key); + + PerformOperation(key, new AVIncrementOperation(amount)); + } + } + + /// + /// Atomically increments the given key by the given number. + /// + /// The key to increment. + /// The amount to increment by. + public void Increment(string key, double amount) + { + lock (mutex) + { + CheckKeyIsMutable(key); + + PerformOperation(key, new AVIncrementOperation(amount)); + } + } + + #endregion + + /// + /// Atomically adds an object to the end of the list associated with the given key. + /// + /// The key. + /// The object to add. + public void AddToList(string key, object value) + { + AddRangeToList(key, new[] { value }); + } + + /// + /// Atomically adds objects to the end of the list associated with the given key. + /// + /// The key. + /// The objects to add. + public void AddRangeToList(string key, IEnumerable values) + { + lock (mutex) + { + CheckKeyIsMutable(key); + + PerformOperation(key, new AVAddOperation(values.Cast())); + + OnCollectionPropertyUpdated(key, NotifyCollectionUpdatedAction.Add, null, values); + } + } + + /// + /// Atomically adds an object to the end of the list associated with the given key, + /// only if it is not already present in the list. The position of the insert is not + /// guaranteed. + /// + /// The key. + /// The object to add. + public void AddUniqueToList(string key, object value) + { + AddRangeUniqueToList(key, new object[] { value }); + } + + /// + /// Atomically adds objects to the end of the list associated with the given key, + /// only if they are not already present in the list. The position of the inserts are not + /// guaranteed. + /// + /// The key. + /// The objects to add. + public void AddRangeUniqueToList(string key, IEnumerable values) + { + lock (mutex) + { + CheckKeyIsMutable(key); + + PerformOperation(key, new AVAddUniqueOperation(values.Cast())); + } + } + + /// + /// Atomically removes all instances of the objects in + /// from the list associated with the given key. + /// + /// The key. + /// The objects to remove. + public void RemoveAllFromList(string key, IEnumerable values) + { + lock (mutex) + { + CheckKeyIsMutable(key); + + PerformOperation(key, new AVRemoveOperation(values.Cast())); + + OnCollectionPropertyUpdated(key, NotifyCollectionUpdatedAction.Remove, values, null); + } + } + + /// + /// Returns whether this object has a particular key. + /// + /// The key to check for + public bool ContainsKey(string key) + { + lock (mutex) + { + return estimatedData.ContainsKey(key); + } + } + + /// + /// Gets a value for the key of a particular type. + /// The type to convert the value to. Supported types are + /// AVObject and its descendents, LeanCloud types such as AVRelation and AVGeopoint, + /// primitive types,IList<T>, IDictionary<string, T>, and strings. + /// The key of the element to get. + /// The property is + /// retrieved and is not found. + /// + public T Get(string key) + { + return Conversion.To(this[key]); + } + + /// + /// Access or create a Relation value for a key. + /// + /// The type of object to create a relation for. + /// The key for the relation field. + /// A AVRelation for the key. + public AVRelation GetRelation(string key) where T : AVObject + { + // All the sanity checking is done when add or remove is called. + AVRelation relation = null; + TryGetValue(key, out relation); + return relation ?? new AVRelation(this, key); + } + + /// + /// Get relation revserse query. + /// + /// AVObject + /// parent className + /// key + /// + public AVQuery GetRelationRevserseQuery(string parentClassName, string key) where T : AVObject + { + if (string.IsNullOrEmpty(parentClassName)) + { + throw new ArgumentNullException("parentClassName", "can not query a relation without parentClassName."); + } + if (string.IsNullOrEmpty(key)) + { + throw new ArgumentNullException("key", "can not query a relation without key."); + } + return new AVQuery(parentClassName).WhereEqualTo(key, this); + } + + /// + /// Populates result with the value for the key, if possible. + /// + /// The desired type for the value. + /// The key to retrieve a value for. + /// The value for the given key, converted to the + /// requested type, or null if unsuccessful. + /// true if the lookup and conversion succeeded, otherwise + /// false. + public virtual bool TryGetValue(string key, out T result) + { + lock (mutex) + { + if (ContainsKey(key)) + { + try + { + var temp = Conversion.To(this[key]); + result = temp; + return true; + } + catch (InvalidCastException) + { + result = default(T); + return false; + } + } + result = default(T); + return false; + } + } + + /// + /// Gets whether the AVObject has been fetched. + /// + public bool IsDataAvailable + { + get + { + lock (mutex) + { + return hasBeenFetched; + } + } + } + + private bool CheckIsDataAvailable(string key) + { + lock (mutex) + { + return IsDataAvailable || estimatedData.ContainsKey(key); + } + } + + private void CheckGetAccess(string key) + { + lock (mutex) + { + if (!CheckIsDataAvailable(key)) + { + throw new InvalidOperationException( + "AVObject has no data for this key. Call FetchIfNeededAsync() to get the data."); + } + } + } + + private void CheckKeyIsMutable(string key) + { + if (!IsKeyMutable(key)) + { + throw new InvalidOperationException( + "Cannot change the `" + key + "` property of a `" + ClassName + "` object."); + } + } + + protected virtual bool IsKeyMutable(string key) + { + return true; + } + + /// + /// A helper function for checking whether two AVObjects point to + /// the same object in the cloud. + /// + public bool HasSameId(AVObject other) + { + lock (mutex) + { + return other != null && + object.Equals(ClassName, other.ClassName) && + object.Equals(ObjectId, other.ObjectId); + } + } + + internal IDictionary CurrentOperations + { + get + { + lock (mutex) + { + return operationSetQueue.Last.Value; + } + } + } + + /// + /// Gets a set view of the keys contained in this object. This does not include createdAt, + /// updatedAt, or objectId. It does include things like username and ACL. + /// + public ICollection Keys + { + get + { + lock (mutex) + { + return estimatedData.Keys; + } + } + } + + /// + /// Gets or sets the AVACL governing this object. + /// + [AVFieldName("ACL")] + public AVACL ACL + { + get { return GetProperty(null, "ACL"); } + set { SetProperty(value, "ACL"); } + } + + /// + /// Returns true if this object was created by the LeanCloud server when the + /// object might have already been there (e.g. in the case of a Facebook + /// login) + /// +#if !UNITY + public +#else + internal +#endif + bool IsNew + { + get + { + return state.IsNew; + } +#if !UNITY + internal +#endif + set + { + MutateState(mutableClone => + { + mutableClone.IsNew = value; + }); + OnPropertyChanged("IsNew"); + } + } + + /// + /// Gets the last time this object was updated as the server sees it, so that if you make changes + /// to a AVObject, then wait a while, and then call , the updated time + /// will be the time of the call rather than the time the object was + /// changed locally. + /// + [AVFieldName("updatedAt")] + public DateTime? UpdatedAt + { + get + { + return state.UpdatedAt; + } + } + + /// + /// Gets the first time this object was saved as the server sees it, so that if you create a + /// AVObject, then wait a while, and then call , the + /// creation time will be the time of the first call rather than + /// the time the object was created locally. + /// + [AVFieldName("createdAt")] + public DateTime? CreatedAt + { + get + { + return state.CreatedAt; + } + } + + /// + /// Indicates whether this AVObject has unsaved changes. + /// + public bool IsDirty + { + get + { + lock (mutex) { return CheckIsDirty(true); } + } + internal set + { + lock (mutex) + { + dirty = value; + OnPropertyChanged("IsDirty"); + } + } + } + + /// + /// Indicates whether key is unsaved for this AVObject. + /// + /// The key to check for. + /// true if the key has been altered and not saved yet, otherwise + /// false. + public bool IsKeyDirty(string key) + { + lock (mutex) + { + return CurrentOperations.ContainsKey(key); + } + } + + private bool CheckIsDirty(bool considerChildren) + { + lock (mutex) + { + return (dirty || CurrentOperations.Count > 0 || (considerChildren && HasDirtyChildren)); + } + } + + /// + /// Gets or sets the object id. An object id is assigned as soon as an object is + /// saved to the server. The combination of a and an + /// uniquely identifies an object in your application. + /// + [AVFieldName("objectId")] + public string ObjectId + { + get + { + return state.ObjectId; + } + set + { + IsDirty = true; + SetObjectIdInternal(value); + } + } + /// + /// Sets the objectId without marking dirty. + /// + /// The new objectId + private void SetObjectIdInternal(string objectId) + { + lock (mutex) + { + MutateState(mutableClone => + { + mutableClone.ObjectId = objectId; + }); + OnPropertyChanged("ObjectId"); + } + } + + /// + /// Gets the class name for the AVObject. + /// + public string ClassName + { + get + { + return state.ClassName; + } + } + + /// + /// Adds a value for the given key, throwing an Exception if the key + /// already has a value. + /// + /// + /// This allows you to use collection initialization syntax when creating AVObjects, + /// such as: + /// + /// var obj = new AVObject("MyType") + /// { + /// {"name", "foo"}, + /// {"count", 10}, + /// {"found", false} + /// }; + /// + /// + /// The key for which a value should be set. + /// The value for the key. + public void Add(string key, object value) + { + lock (mutex) + { + if (this.ContainsKey(key)) + { + throw new ArgumentException("Key already exists", key); + } + this[key] = value; + } + } + + IEnumerator> IEnumerable> + .GetEnumerator() + { + lock (mutex) + { + return estimatedData.GetEnumerator(); + } + } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + lock (mutex) + { + return ((IEnumerable>)this).GetEnumerator(); + } + } + + /// + /// Gets a for the type of object specified by + /// + /// + /// The class name of the object. + /// A new . + public static AVQuery GetQuery(string className) + { + // Since we can't return a AVQuery (due to strong-typing with + // generics), we'll require you to go through subclasses. This is a better + // experience anyway, especially with LINQ integration, since you'll get + // strongly-typed queries and compile-time checking of property names and + // types. + if (SubclassingController.GetType(className) != null) + { + throw new ArgumentException( + "Use the class-specific query properties for class " + className, "className"); + } + return new AVQuery(className); + } + + /// + /// Raises change notifications for all properties associated with the given + /// field names. If fieldNames is null, this will notify for all known field-linked + /// properties (e.g. this happens when we recalculate all estimated data from scratch) + /// + protected virtual void OnFieldsChanged(IEnumerable fieldNames) + { + var mappings = SubclassingController.GetPropertyMappings(ClassName); + IEnumerable properties; + + if (fieldNames != null && mappings != null) + { + properties = from m in mappings + join f in fieldNames on m.Value equals f + select m.Key; + } + else if (mappings != null) + { + properties = mappings.Keys; + } + else + { + properties = Enumerable.Empty(); + } + + foreach (var property in properties) + { + OnPropertyChanged(property); + } + OnPropertyChanged("Item[]"); + } + + /// + /// Raises change notifications for a property. Passing null or the empty string + /// notifies the binding framework that all properties/indexes have changed. + /// Passing "Item[]" tells the binding framework that all indexed values + /// have changed (but not all properties) + /// + protected virtual void OnPropertyChanged( +#if !UNITY +[CallerMemberName] string propertyName = null +#else +string propertyName +#endif +) + { + propertyChanged.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + private SynchronizedEventHandler propertyChanged = + new SynchronizedEventHandler(); + /// + /// Occurs when a property value changes. + /// + public event PropertyChangedEventHandler PropertyChanged + { + add + { + propertyChanged.Add(value); + } + remove + { + propertyChanged.Remove(value); + } + } + + private SynchronizedEventHandler propertyUpdated = + new SynchronizedEventHandler(); + + public event PropertyUpdatedEventHandler PropertyUpdated + { + add + { + propertyUpdated.Add(value); + } + remove + { + propertyUpdated.Remove(value); + } + } + + protected virtual void OnPropertyUpdated(string propertyName, object newValue, object oldValue) + { + propertyUpdated.Invoke(this, new PropertyUpdatedEventArgs(propertyName, oldValue, newValue)); + } + + private SynchronizedEventHandler collectionUpdated = +new SynchronizedEventHandler(); + + public event CollectionPropertyUpdatedEventHandler CollectionPropertyUpdated + { + add + { + collectionUpdated.Add(value); + } + remove + { + collectionUpdated.Remove(value); + } + } + + protected virtual void OnCollectionPropertyUpdated(string propertyName, NotifyCollectionUpdatedAction action, IEnumerable oldValues, IEnumerable newValues) + { + collectionUpdated?.Invoke(this, new CollectionPropertyUpdatedEventArgs(propertyName, action, oldValues, newValues)); + } + } + + public interface INotifyPropertyUpdated + { + event PropertyUpdatedEventHandler PropertyUpdated; + } + + public interface INotifyCollectionPropertyUpdated + { + event CollectionPropertyUpdatedEventHandler CollectionPropertyUpdated; + } + + public enum NotifyCollectionUpdatedAction + { + Add, + Remove + } + + public class CollectionPropertyUpdatedEventArgs : PropertyChangedEventArgs + { + public CollectionPropertyUpdatedEventArgs(string propertyName, NotifyCollectionUpdatedAction collectionAction, IEnumerable oldValues, IEnumerable newValues) : base(propertyName) + { + CollectionAction = collectionAction; + OldValues = oldValues; + NewValues = newValues; + } + + public IEnumerable OldValues { get; set; } + + public IEnumerable NewValues { get; set; } + + public NotifyCollectionUpdatedAction CollectionAction { get; set; } + } + + public class PropertyUpdatedEventArgs : PropertyChangedEventArgs + { + public PropertyUpdatedEventArgs(string propertyName, object oldValue, object newValue) : base(propertyName) + { + OldValue = oldValue; + NewValue = newValue; + } + + public object OldValue { get; private set; } + public object NewValue { get; private set; } + } + + public delegate void PropertyUpdatedEventHandler(object sender, PropertyUpdatedEventArgs args); + + public delegate void CollectionPropertyUpdatedEventHandler(object sender, CollectionPropertyUpdatedEventArgs args); +} diff --git a/Storage/Source/Public/AVQuery.cs b/Storage/Source/Public/AVQuery.cs new file mode 100644 index 0000000..5a0b341 --- /dev/null +++ b/Storage/Source/Public/AVQuery.cs @@ -0,0 +1,360 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using LeanCloud.Storage.Internal; + +namespace LeanCloud +{ + + /// + /// The AVQuery class defines a query that is used to fetch AVObjects. The + /// most common use case is finding all objects that match a query through the + /// method. + /// + /// + /// This sample code fetches all objects of + /// class "MyClass": + /// + /// + /// AVQuery query = new AVQuery("MyClass"); + /// IEnumerable<AVObject> result = await query.FindAsync(); + /// + /// + /// A AVQuery can also be used to retrieve a single object whose id is known, + /// through the method. For example, this sample code + /// fetches an object of class "MyClass" and id myId. + /// + /// + /// AVQuery query = new AVQuery("MyClass"); + /// AVObject result = await query.GetAsync(myId); + /// + /// + /// A AVQuery can also be used to count the number of objects that match the + /// query without retrieving all of those objects. For example, this sample code + /// counts the number of objects of the class "MyClass". + /// + /// + /// AVQuery query = new AVQuery("MyClass"); + /// int count = await query.CountAsync(); + /// + /// + public class AVQuery : AVQueryPair, T>, IAVQuery + where T : AVObject + { + internal static IAVQueryController QueryController + { + get + { + return AVPlugins.Instance.QueryController; + } + } + + internal static IObjectSubclassingController SubclassingController + { + get + { + return AVPlugins.Instance.SubclassingController; + } + } + + /// + /// 调试时可以用来查看最终的发送的查询语句 + /// + private string JsonString + { + get + { + return AVClient.SerializeJsonString(this.BuildParameters(true)); + } + } + + /// + /// Private constructor for composition of queries. A Source query is required, + /// but the remaining values can be null if they won't be changed in this + /// composition. + /// + private AVQuery(AVQuery source, + IDictionary where = null, + IEnumerable replacementOrderBy = null, + IEnumerable thenBy = null, + int? skip = null, + int? limit = null, + IEnumerable includes = null, + IEnumerable selectedKeys = null, + String redirectClassNameForKey = null) + : base(source, where, replacementOrderBy, thenBy, skip, limit, includes, selectedKeys, redirectClassNameForKey) + { + + } + + //internal override AVQuery CreateInstance( + // AVQuery source, + // IDictionary where = null, + // IEnumerable replacementOrderBy = null, + // IEnumerable thenBy = null, + // int? skip = null, + // int? limit = null, + // IEnumerable includes = null, + // IEnumerable selectedKeys = null, + // String redirectClassNameForKey = null) + //{ + // return new AVQuery(this, where, replacementOrderBy, thenBy, skip, limit, includes, selectedKeys, redirectClassNameForKey); + //} + + public override AVQuery CreateInstance( + IDictionary where = null, + IEnumerable replacementOrderBy = null, + IEnumerable thenBy = null, + int? skip = null, + int? limit = null, + IEnumerable includes = null, + IEnumerable selectedKeys = null, + String redirectClassNameForKey = null) + { + return new AVQuery(this, where, replacementOrderBy, thenBy, skip, limit, includes, selectedKeys, redirectClassNameForKey); + } + + + /// + /// Constructs a query based upon the AVObject subclass used as the generic parameter for the AVQuery. + /// + public AVQuery() + : this(SubclassingController.GetClassName(typeof(T))) + { + } + + /// + /// Constructs a query. A default query with no further parameters will retrieve + /// all s of the provided class. + /// + /// The name of the class to retrieve AVObjects for. + public AVQuery(string className) + : base(className) + { + + } + + /// + /// Constructs a query that is the or of the given queries. + /// + /// The list of AVQueries to 'or' together. + /// A AVQquery that is the 'or' of the passed in queries. + public static AVQuery Or(IEnumerable> queries) + { + string className = null; + var orValue = new List>(); + // We need to cast it to non-generic IEnumerable because of AOT-limitation + var nonGenericQueries = (IEnumerable)queries; + foreach (var obj in nonGenericQueries) + { + var q = (AVQuery)obj; + if (className != null && q.className != className) + { + throw new ArgumentException( + "All of the queries in an or query must be on the same class."); + } + className = q.className; + var parameters = q.BuildParameters(); + if (parameters.Count == 0) + { + continue; + } + object where; + if (!parameters.TryGetValue("where", out where) || parameters.Count > 1) + { + throw new ArgumentException( + "None of the queries in an or query can have non-filtering clauses"); + } + orValue.Add(where as IDictionary); + } + return new AVQuery(new AVQuery(className), + where: new Dictionary { + { "$or", orValue} + }); + } + + /// + /// Retrieves a list of AVObjects that satisfy this query from LeanCloud. + /// + /// The cancellation token. + /// The list of AVObjects that match this query. + public override Task> FindAsync(CancellationToken cancellationToken) + { + return AVUser.GetCurrentUserAsync().OnSuccess(t => + { + return QueryController.FindAsync(this, t.Result, cancellationToken); + }).Unwrap().OnSuccess(t => + { + IEnumerable states = t.Result; + return (from state in states + select AVObject.FromState(state, ClassName)); + }); + } + + /// + /// Retrieves at most one AVObject that satisfies this query. + /// + /// The cancellation token. + /// A single AVObject that satisfies this query, or else null. + public override Task FirstOrDefaultAsync(CancellationToken cancellationToken) + { + return AVUser.GetCurrentUserAsync().OnSuccess(t => + { + return QueryController.FirstAsync(this, t.Result, cancellationToken); + }).Unwrap().OnSuccess(t => + { + IObjectState state = t.Result; + return state == null ? default(T) : AVObject.FromState(state, ClassName); + }); + } + + /// + /// Retrieves at most one AVObject that satisfies this query. + /// + /// The cancellation token. + /// A single AVObject that satisfies this query. + /// If no results match the query. + public override Task FirstAsync(CancellationToken cancellationToken) + { + return FirstOrDefaultAsync(cancellationToken).OnSuccess(t => + { + if (t.Result == null) + { + throw new AVException(AVException.ErrorCode.ObjectNotFound, + "No results matched the query."); + } + return t.Result; + }); + } + + /// + /// Counts the number of objects that match this query. + /// + /// The cancellation token. + /// The number of objects that match this query. + public override Task CountAsync(CancellationToken cancellationToken) + { + return AVUser.GetCurrentUserAsync().OnSuccess(t => + { + return QueryController.CountAsync(this, t.Result, cancellationToken); + }).Unwrap(); + } + + /// + /// Constructs a AVObject whose id is already known by fetching data + /// from the server. + /// + /// ObjectId of the AVObject to fetch. + /// The cancellation token. + /// The AVObject for the given objectId. + public override Task GetAsync(string objectId, CancellationToken cancellationToken) + { + AVQuery singleItemQuery = new AVQuery(className) + .WhereEqualTo("objectId", objectId); + singleItemQuery = new AVQuery(singleItemQuery, includes: this.includes, selectedKeys: this.selectedKeys, limit: 1); + return singleItemQuery.FindAsync(cancellationToken).OnSuccess(t => + { + var result = t.Result.FirstOrDefault(); + if (result == null) + { + throw new AVException(AVException.ErrorCode.ObjectNotFound, + "Object with the given objectId not found."); + } + return result; + }); + } + + #region CQL + /// + /// 执行 CQL 查询 + /// + /// CQL 语句 + /// CancellationToken + /// 返回符合条件的对象集合 + public static Task> DoCloudQueryAsync(string cql, CancellationToken cancellationToken) + { + var queryString = string.Format("cloudQuery?cql={0}", Uri.EscapeDataString(cql)); + + return rebuildObjectFromCloudQueryResult(queryString); + } + + /// + /// 执行 CQL 查询 + /// + /// + /// + public static Task> DoCloudQueryAsync(string cql) + { + return DoCloudQueryAsync(cql, CancellationToken.None); + } + + /// + /// 执行 CQL 查询 + /// + /// 带有占位符的模板 cql 语句 + /// 占位符对应的参数数组 + /// + public static Task> DoCloudQueryAsync(string cqlTeamplate, params object[] pvalues) + { + string queryStringTemplate = "cloudQuery?cql={0}&pvalues={1}"; + string pSrting = Json.Encode(pvalues); + string queryString = string.Format(queryStringTemplate, Uri.EscapeDataString(cqlTeamplate), Uri.EscapeDataString(pSrting)); + + return rebuildObjectFromCloudQueryResult(queryString); + } + + internal static Task> rebuildObjectFromCloudQueryResult(string queryString) + { + var command = new AVCommand(queryString, + method: "GET", + sessionToken: AVUser.CurrentSessionToken, + data: null); + + return AVPlugins.Instance.CommandRunner.RunCommandAsync(command, cancellationToken: CancellationToken.None).OnSuccess(t => + { + var items = t.Result.Item2["results"] as IList; + var className = t.Result.Item2["className"].ToString(); + + IEnumerable states = (from item in items + select AVObjectCoder.Instance.Decode(item as IDictionary, AVDecoder.Instance)); + + return (from state in states + select AVObject.FromState(state, className)); + }); + } + + #endregion + + /// + /// Determines whether the specified object is equal to the current object. + /// + /// The object to compare with the current object. + /// true if the specified object is equal to the current object; otherwise, false + public override bool Equals(object obj) + { + if (obj == null || !(obj is AVQuery)) + { + return false; + } + + var other = obj as AVQuery; + return Object.Equals(this.className, other.ClassName) && + this.where.CollectionsEqual(other.where) && + this.orderBy.CollectionsEqual(other.orderBy) && + this.includes.CollectionsEqual(other.includes) && + this.selectedKeys.CollectionsEqual(other.selectedKeys) && + Object.Equals(this.skip, other.skip) && + Object.Equals(this.limit, other.limit); + } + + public override int GetHashCode() + { + return base.GetHashCode(); + } + } +} diff --git a/Storage/Source/Public/AVQueryExtensions.cs b/Storage/Source/Public/AVQueryExtensions.cs new file mode 100644 index 0000000..c726b41 --- /dev/null +++ b/Storage/Source/Public/AVQueryExtensions.cs @@ -0,0 +1,818 @@ +using LeanCloud.Storage.Internal; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; + +namespace LeanCloud +{ + /// + /// Provides extension methods for to support + /// Linq-style queries. + /// + public static class AVQueryExtensions + { + private static readonly MethodInfo getMethod; + private static readonly MethodInfo stringContains; + private static readonly MethodInfo stringStartsWith; + private static readonly MethodInfo stringEndsWith; + private static readonly MethodInfo containsMethod; + private static readonly MethodInfo notContainsMethod; + private static readonly MethodInfo containsKeyMethod; + private static readonly MethodInfo notContainsKeyMethod; + private static readonly Dictionary functionMappings; + static AVQueryExtensions() + { + getMethod = GetMethod(obj => obj.Get(null)).GetGenericMethodDefinition(); + stringContains = GetMethod(str => str.Contains(null)); + stringStartsWith = GetMethod(str => str.StartsWith(null)); + stringEndsWith = GetMethod(str => str.EndsWith(null)); + functionMappings = new Dictionary { + { + stringContains, + GetMethod>(q => q.WhereContains(null, null)) + }, + { + stringStartsWith, + GetMethod>(q => q.WhereStartsWith(null, null)) + }, + { + stringEndsWith, + GetMethod>(q => q.WhereEndsWith(null,null)) + }, + }; + containsMethod = GetMethod( + o => AVQueryExtensions.ContainsStub(null, null)).GetGenericMethodDefinition(); + notContainsMethod = GetMethod( + o => AVQueryExtensions.NotContainsStub(null, null)) + .GetGenericMethodDefinition(); + + containsKeyMethod = GetMethod(o => AVQueryExtensions.ContainsKeyStub(null, null)); + notContainsKeyMethod = GetMethod( + o => AVQueryExtensions.NotContainsKeyStub(null, null)); + } + + /// + /// Gets a MethodInfo for a top-level method call. + /// + private static MethodInfo GetMethod(Expression> expression) + { + return (expression.Body as MethodCallExpression).Method; + } + + /// + /// When a query is normalized, this is a placeholder to indicate we should + /// add a WhereContainedIn() clause. + /// + private static bool ContainsStub(object collection, T value) + { + throw new NotImplementedException( + "Exists only for expression translation as a placeholder."); + } + + /// + /// When a query is normalized, this is a placeholder to indicate we should + /// add a WhereNotContainedIn() clause. + /// + private static bool NotContainsStub(object collection, T value) + { + throw new NotImplementedException( + "Exists only for expression translation as a placeholder."); + } + + /// + /// When a query is normalized, this is a placeholder to indicate that we should + /// add a WhereExists() clause. + /// + private static bool ContainsKeyStub(AVObject obj, string key) + { + throw new NotImplementedException( + "Exists only for expression translation as a placeholder."); + } + + /// + /// When a query is normalized, this is a placeholder to indicate that we should + /// add a WhereDoesNotExist() clause. + /// + private static bool NotContainsKeyStub(AVObject obj, string key) + { + throw new NotImplementedException( + "Exists only for expression translation as a placeholder."); + } + + /// + /// Evaluates an expression and throws if the expression has components that can't be + /// evaluated (e.g. uses the parameter that's only represented by an object on the server). + /// + private static object GetValue(Expression exp) + { + try + { + return Expression.Lambda( + typeof(Func<>).MakeGenericType(exp.Type), exp).Compile().DynamicInvoke(); + } + catch (Exception e) + { + throw new InvalidOperationException("Unable to evaluate expression: " + exp, e); + } + } + + /// + /// Checks whether the MethodCallExpression is a call to AVObject.Get(), + /// which is the call we normalize all indexing into the AVObject to. + /// + private static bool IsAVObjectGet(MethodCallExpression node) + { + if (node == null || node.Object == null) + { + return false; + } + if (!typeof(AVObject).GetTypeInfo().IsAssignableFrom(node.Object.Type.GetTypeInfo())) + { + return false; + } + return node.Method.IsGenericMethod && node.Method.GetGenericMethodDefinition() == getMethod; + } + + + /// + /// Visits an Expression, converting AVObject.Get/AVObject[]/AVObject.Property, + /// and nested indices into a single call to AVObject.Get() with a "field path" like + /// "foo.bar.baz" + /// + private class ObjectNormalizer : ExpressionVisitor + { + protected override Expression VisitIndex(IndexExpression node) + { + var visitedObject = Visit(node.Object); + var indexer = visitedObject as MethodCallExpression; + if (IsAVObjectGet(indexer)) + { + var indexValue = GetValue(node.Arguments[0]) as string; + if (indexValue == null) + { + throw new InvalidOperationException("Index must be a string"); + } + var newPath = GetValue(indexer.Arguments[0]) + "." + indexValue; + return Expression.Call(indexer.Object, + getMethod.MakeGenericMethod(node.Type), + Expression.Constant(newPath, typeof(string))); + } + return base.VisitIndex(node); + } + + /// + /// Check for a AVFieldName attribute and use that as the path component, turning + /// properties like foo.ObjectId into foo.Get("objectId") + /// + protected override Expression VisitMember(MemberExpression node) + { + var fieldName = node.Member.GetCustomAttribute(); + if (fieldName != null && + typeof(AVObject).GetTypeInfo().IsAssignableFrom(node.Expression.Type.GetTypeInfo())) + { + var newPath = fieldName.FieldName; + return Expression.Call(node.Expression, + getMethod.MakeGenericMethod(node.Type), + Expression.Constant(newPath, typeof(string))); + } + return base.VisitMember(node); + } + + /// + /// If a AVObject.Get() call has been cast, just change the generic parameter. + /// + protected override Expression VisitUnary(UnaryExpression node) + { + var methodCall = Visit(node.Operand) as MethodCallExpression; + if ((node.NodeType == ExpressionType.Convert || + node.NodeType == ExpressionType.ConvertChecked) && + IsAVObjectGet(methodCall)) + { + return Expression.Call(methodCall.Object, + getMethod.MakeGenericMethod(node.Type), + methodCall.Arguments); + } + return base.VisitUnary(node); + } + + protected override Expression VisitMethodCall(MethodCallExpression node) + { + if (node.Method.Name == "get_Item" && node.Object is ParameterExpression) + { + var indexPath = GetValue(node.Arguments[0]) as string; + return Expression.Call(node.Object, + getMethod.MakeGenericMethod(typeof(object)), + Expression.Constant(indexPath, typeof(string))); + } + + if (node.Method.Name == "get_Item" || IsAVObjectGet(node)) + { + var visitedObject = Visit(node.Object); + var indexer = visitedObject as MethodCallExpression; + if (IsAVObjectGet(indexer)) + { + var indexValue = GetValue(node.Arguments[0]) as string; + if (indexValue == null) + { + throw new InvalidOperationException("Index must be a string"); + } + var newPath = GetValue(indexer.Arguments[0]) + "." + indexValue; + return Expression.Call(indexer.Object, + getMethod.MakeGenericMethod(node.Type), + Expression.Constant(newPath, typeof(string))); + } + } + return base.VisitMethodCall(node); + } + } + + /// + /// Normalizes Where expressions. + /// + private class WhereNormalizer : ExpressionVisitor + { + + /// + /// Normalizes binary operators. <, >, <=, >= !=, and == + /// This puts the AVObject.Get() on the left side of the operation + /// (reversing it if necessary), and normalizes the AVObject.Get() + /// + protected override Expression VisitBinary(BinaryExpression node) + { + var leftTransformed = new ObjectNormalizer().Visit(node.Left) as MethodCallExpression; + var rightTransformed = new ObjectNormalizer().Visit(node.Right) as MethodCallExpression; + + MethodCallExpression objectExpression; + Expression filterExpression; + bool inverted; + if (leftTransformed != null) + { + objectExpression = leftTransformed; + filterExpression = node.Right; + inverted = false; + } + else + { + objectExpression = rightTransformed; + filterExpression = node.Left; + inverted = true; + } + + try + { + switch (node.NodeType) + { + case ExpressionType.GreaterThan: + if (inverted) + { + return Expression.LessThan(objectExpression, filterExpression); + } + else + { + return Expression.GreaterThan(objectExpression, filterExpression); + } + case ExpressionType.GreaterThanOrEqual: + if (inverted) + { + return Expression.LessThanOrEqual(objectExpression, filterExpression); + } + else + { + return Expression.GreaterThanOrEqual(objectExpression, filterExpression); + } + case ExpressionType.LessThan: + if (inverted) + { + return Expression.GreaterThan(objectExpression, filterExpression); + } + else + { + return Expression.LessThan(objectExpression, filterExpression); + } + case ExpressionType.LessThanOrEqual: + if (inverted) + { + return Expression.GreaterThanOrEqual(objectExpression, filterExpression); + } + else + { + return Expression.LessThanOrEqual(objectExpression, filterExpression); + } + case ExpressionType.Equal: + return Expression.Equal(objectExpression, filterExpression); + case ExpressionType.NotEqual: + return Expression.NotEqual(objectExpression, filterExpression); + } + } + catch (ArgumentException) + { + throw new InvalidOperationException("Operation not supported: " + node); + } + return base.VisitBinary(node); + } + + /// + /// If a ! operator is used, this removes the ! and instead calls the equivalent + /// function (so e.g. == becomes !=, < becomes >=, Contains becomes NotContains) + /// + protected override Expression VisitUnary(UnaryExpression node) + { + // Normalizes inversion + if (node.NodeType == ExpressionType.Not) + { + var visitedOperand = Visit(node.Operand); + var binaryOperand = visitedOperand as BinaryExpression; + if (binaryOperand != null) + { + switch (binaryOperand.NodeType) + { + case ExpressionType.GreaterThan: + return Expression.LessThanOrEqual(binaryOperand.Left, binaryOperand.Right); + case ExpressionType.GreaterThanOrEqual: + return Expression.LessThan(binaryOperand.Left, binaryOperand.Right); + case ExpressionType.LessThan: + return Expression.GreaterThanOrEqual(binaryOperand.Left, binaryOperand.Right); + case ExpressionType.LessThanOrEqual: + return Expression.GreaterThan(binaryOperand.Left, binaryOperand.Right); + case ExpressionType.Equal: + return Expression.NotEqual(binaryOperand.Left, binaryOperand.Right); + case ExpressionType.NotEqual: + return Expression.Equal(binaryOperand.Left, binaryOperand.Right); + } + } + + var methodCallOperand = visitedOperand as MethodCallExpression; + if (methodCallOperand != null) + { + if (methodCallOperand.Method.IsGenericMethod) + { + if (methodCallOperand.Method.GetGenericMethodDefinition() == containsMethod) + { + var genericNotContains = notContainsMethod.MakeGenericMethod( + methodCallOperand.Method.GetGenericArguments()); + return Expression.Call(genericNotContains, methodCallOperand.Arguments.ToArray()); + } + if (methodCallOperand.Method.GetGenericMethodDefinition() == notContainsMethod) + { + var genericContains = containsMethod.MakeGenericMethod( + methodCallOperand.Method.GetGenericArguments()); + return Expression.Call(genericContains, methodCallOperand.Arguments.ToArray()); + } + } + if (methodCallOperand.Method == containsKeyMethod) + { + return Expression.Call(notContainsKeyMethod, methodCallOperand.Arguments.ToArray()); + } + if (methodCallOperand.Method == notContainsKeyMethod) + { + return Expression.Call(containsKeyMethod, methodCallOperand.Arguments.ToArray()); + } + } + } + return base.VisitUnary(node); + } + + /// + /// Normalizes .Equals into == and Contains() into the appropriate stub. + /// + protected override Expression VisitMethodCall(MethodCallExpression node) + { + // Convert .Equals() into == + if (node.Method.Name == "Equals" && + node.Method.ReturnType == typeof(bool) && + node.Method.GetParameters().Length == 1) + { + var obj = new ObjectNormalizer().Visit(node.Object) as MethodCallExpression; + var parameter = new ObjectNormalizer().Visit(node.Arguments[0]) as MethodCallExpression; + if ((IsAVObjectGet(obj) && (obj.Object is ParameterExpression)) || + (IsAVObjectGet(parameter) && (parameter.Object is ParameterExpression))) + { + return Expression.Equal(node.Object, node.Arguments[0]); + } + } + + // Convert the .Contains() into a ContainsStub + if (node.Method != stringContains && + node.Method.Name == "Contains" && + node.Method.ReturnType == typeof(bool) && + node.Method.GetParameters().Length <= 2) + { + var collection = node.Method.GetParameters().Length == 1 ? + node.Object : + node.Arguments[0]; + var parameterIndex = node.Method.GetParameters().Length - 1; + var parameter = new ObjectNormalizer().Visit(node.Arguments[parameterIndex]) + as MethodCallExpression; + if (IsAVObjectGet(parameter) && (parameter.Object is ParameterExpression)) + { + var genericContains = containsMethod.MakeGenericMethod(parameter.Type); + return Expression.Call(genericContains, collection, parameter); + } + var target = new ObjectNormalizer().Visit(collection) as MethodCallExpression; + var element = node.Arguments[parameterIndex]; + if (IsAVObjectGet(target) && (target.Object is ParameterExpression)) + { + var genericContains = containsMethod.MakeGenericMethod(element.Type); + return Expression.Call(genericContains, target, element); + } + } + + // Convert obj["foo.bar"].ContainsKey("baz") into obj.ContainsKey("foo.bar.baz") + if (node.Method.Name == "ContainsKey" && + node.Method.ReturnType == typeof(bool) && + node.Method.GetParameters().Length == 1) + { + var getter = new ObjectNormalizer().Visit(node.Object) as MethodCallExpression; + Expression target = null; + string path = null; + if (IsAVObjectGet(getter) && getter.Object is ParameterExpression) + { + target = getter.Object; + path = GetValue(getter.Arguments[0]) + "." + GetValue(node.Arguments[0]); + return Expression.Call(containsKeyMethod, target, Expression.Constant(path)); + } + else if (node.Object is ParameterExpression) + { + target = node.Object; + path = GetValue(node.Arguments[0]) as string; + } + if (target != null && path != null) + { + return Expression.Call(containsKeyMethod, target, Expression.Constant(path)); + } + } + return base.VisitMethodCall(node); + } + } + + /// + /// Converts a normalized method call expression into the appropriate AVQuery clause. + /// + private static AVQuery WhereMethodCall( + this AVQuery source, Expression> expression, MethodCallExpression node) + where T : AVObject + { + if (IsAVObjectGet(node) && (node.Type == typeof(bool) || node.Type == typeof(bool?))) + { + // This is a raw boolean field access like 'where obj.Get("foo")' + return source.WhereEqualTo(GetValue(node.Arguments[0]) as string, true); + } + + MethodInfo translatedMethod; + if (functionMappings.TryGetValue(node.Method, out translatedMethod)) + { + var objTransformed = new ObjectNormalizer().Visit(node.Object) as MethodCallExpression; + if (!(IsAVObjectGet(objTransformed) && + objTransformed.Object == expression.Parameters[0])) + { + throw new InvalidOperationException( + "The left-hand side of a supported function call must be a AVObject field access."); + } + var fieldPath = GetValue(objTransformed.Arguments[0]); + var containedIn = GetValue(node.Arguments[0]); + var queryType = translatedMethod.DeclaringType.GetGenericTypeDefinition() + .MakeGenericType(typeof(T)); + translatedMethod = ReflectionHelpers.GetMethod(queryType, + translatedMethod.Name, + translatedMethod.GetParameters().Select(p => p.ParameterType).ToArray()); + return translatedMethod.Invoke(source, new[] { fieldPath, containedIn }) as AVQuery; + } + + if (node.Arguments[0] == expression.Parameters[0]) + { + // obj.ContainsKey("foo") --> query.WhereExists("foo") + if (node.Method == containsKeyMethod) + { + return source.WhereExists(GetValue(node.Arguments[1]) as string); + } + // !obj.ContainsKey("foo") --> query.WhereDoesNotExist("foo") + if (node.Method == notContainsKeyMethod) + { + return source.WhereDoesNotExist(GetValue(node.Arguments[1]) as string); + } + } + + if (node.Method.IsGenericMethod) + { + if (node.Method.GetGenericMethodDefinition() == containsMethod) + { + // obj.Get>("path").Contains(someValue) + if (IsAVObjectGet(node.Arguments[0] as MethodCallExpression)) + { + return source.WhereEqualTo( + GetValue(((MethodCallExpression)node.Arguments[0]).Arguments[0]) as string, + GetValue(node.Arguments[1])); + } + // someList.Contains(obj.Get("path")) + if (IsAVObjectGet(node.Arguments[1] as MethodCallExpression)) + { + var collection = GetValue(node.Arguments[0]) as System.Collections.IEnumerable; + return source.WhereContainedIn( + GetValue(((MethodCallExpression)node.Arguments[1]).Arguments[0]) as string, + collection.Cast()); + } + } + + if (node.Method.GetGenericMethodDefinition() == notContainsMethod) + { + // !obj.Get>("path").Contains(someValue) + if (IsAVObjectGet(node.Arguments[0] as MethodCallExpression)) + { + return source.WhereNotEqualTo( + GetValue(((MethodCallExpression)node.Arguments[0]).Arguments[0]) as string, + GetValue(node.Arguments[1])); + } + // !someList.Contains(obj.Get("path")) + if (IsAVObjectGet(node.Arguments[1] as MethodCallExpression)) + { + var collection = GetValue(node.Arguments[0]) as System.Collections.IEnumerable; + return source.WhereNotContainedIn( + GetValue(((MethodCallExpression)node.Arguments[1]).Arguments[0]) as string, + collection.Cast()); + } + } + } + throw new InvalidOperationException(node.Method + " is not a supported method call in a where expression."); + } + + /// + /// Converts a normalized binary expression into the appropriate AVQuery clause. + /// + private static AVQuery WhereBinaryExpression( + this AVQuery source, Expression> expression, BinaryExpression node) + where T : AVObject + { + var leftTransformed = new ObjectNormalizer().Visit(node.Left) as MethodCallExpression; + + if (!(IsAVObjectGet(leftTransformed) && + leftTransformed.Object == expression.Parameters[0])) + { + throw new InvalidOperationException( + "Where expressions must have one side be a field operation on a AVObject."); + } + + var fieldPath = GetValue(leftTransformed.Arguments[0]) as string; + var filterValue = GetValue(node.Right); + + if (filterValue != null && !AVEncoder.IsValidType(filterValue)) + { + throw new InvalidOperationException( + "Where clauses must use types compatible with AVObjects."); + } + + switch (node.NodeType) + { + case ExpressionType.GreaterThan: + return source.WhereGreaterThan(fieldPath, filterValue); + case ExpressionType.GreaterThanOrEqual: + return source.WhereGreaterThanOrEqualTo(fieldPath, filterValue); + case ExpressionType.LessThan: + return source.WhereLessThan(fieldPath, filterValue); + case ExpressionType.LessThanOrEqual: + return source.WhereLessThanOrEqualTo(fieldPath, filterValue); + case ExpressionType.Equal: + return source.WhereEqualTo(fieldPath, filterValue); + case ExpressionType.NotEqual: + return source.WhereNotEqualTo(fieldPath, filterValue); + default: + throw new InvalidOperationException( + "Where expressions do not support this operator."); + } + } + + /// + /// Filters a query based upon the predicate provided. + /// + /// The type of AVObject being queried for. + /// The base to which + /// the predicate will be added. + /// A function to test each AVObject for a condition. + /// The predicate must be able to be represented by one of the standard Where + /// functions on AVQuery + /// A new AVQuery whose results will match the given predicate as + /// well as the Source's filters. + public static AVQuery Where( + this AVQuery source, Expression> predicate) + where TSource : AVObject + { + // Handle top-level logic operators && and || + var binaryExpression = predicate.Body as BinaryExpression; + if (binaryExpression != null) + { + if (binaryExpression.NodeType == ExpressionType.AndAlso) + { + return source + .Where(Expression.Lambda>( + binaryExpression.Left, predicate.Parameters)) + .Where(Expression.Lambda>( + binaryExpression.Right, predicate.Parameters)); + } + if (binaryExpression.NodeType == ExpressionType.OrElse) + { + var left = source.Where(Expression.Lambda>( + binaryExpression.Left, predicate.Parameters)); + var right = source.Where(Expression.Lambda>( + binaryExpression.Right, predicate.Parameters)); + return left.Or(right); + } + } + + var normalized = new WhereNormalizer().Visit(predicate.Body); + + var methodCallExpr = normalized as MethodCallExpression; + if (methodCallExpr != null) + { + return source.WhereMethodCall(predicate, methodCallExpr); + } + + var binaryExpr = normalized as BinaryExpression; + if (binaryExpr != null) + { + return source.WhereBinaryExpression(predicate, binaryExpr); + } + + var unaryExpr = normalized as UnaryExpression; + if (unaryExpr != null && unaryExpr.NodeType == ExpressionType.Not) + { + var node = unaryExpr.Operand as MethodCallExpression; + if (IsAVObjectGet(node) && (node.Type == typeof(bool) || node.Type == typeof(bool?))) + { + // This is a raw boolean field access like 'where !obj.Get("foo")' + return source.WhereNotEqualTo(GetValue(node.Arguments[0]) as string, true); + } + } + + throw new InvalidOperationException( + "Encountered an unsupported expression for AVQueries."); + } + + /// + /// Normalizes an OrderBy's keySelector expression and then extracts the path + /// from the AVObject.Get() call. + /// + private static string GetOrderByPath( + Expression> keySelector) + { + string result = null; + var normalized = new ObjectNormalizer().Visit(keySelector.Body); + var callExpr = normalized as MethodCallExpression; + if (IsAVObjectGet(callExpr) && callExpr.Object == keySelector.Parameters[0]) + { + // We're operating on the parameter + result = GetValue(callExpr.Arguments[0]) as string; + } + if (result == null) + { + throw new InvalidOperationException( + "OrderBy expression must be a field access on a AVObject."); + } + return result; + } + + /// + /// Orders a query based upon the key selector provided. + /// + /// The type of AVObject being queried for. + /// The type of key returned by keySelector. + /// The query to order. + /// A function to extract a key from the AVObject. + /// A new AVQuery based on Source whose results will be ordered by + /// the key specified in the keySelector. + public static AVQuery OrderBy( + this AVQuery source, Expression> keySelector) + where TSource : AVObject + { + return source.OrderBy(GetOrderByPath(keySelector)); + } + + /// + /// Orders a query based upon the key selector provided. + /// + /// The type of AVObject being queried for. + /// The type of key returned by keySelector. + /// The query to order. + /// A function to extract a key from the AVObject. + /// A new AVQuery based on Source whose results will be ordered by + /// the key specified in the keySelector. + public static AVQuery OrderByDescending( + this AVQuery source, Expression> keySelector) + where TSource : AVObject + { + return source.OrderByDescending(GetOrderByPath(keySelector)); + } + + /// + /// Performs a subsequent ordering of a query based upon the key selector provided. + /// + /// The type of AVObject being queried for. + /// The type of key returned by keySelector. + /// The query to order. + /// A function to extract a key from the AVObject. + /// A new AVQuery based on Source whose results will be ordered by + /// the key specified in the keySelector. + public static AVQuery ThenBy( + this AVQuery source, Expression> keySelector) + where TSource : AVObject + { + return source.ThenBy(GetOrderByPath(keySelector)); + } + + /// + /// Performs a subsequent ordering of a query based upon the key selector provided. + /// + /// The type of AVObject being queried for. + /// The type of key returned by keySelector. + /// The query to order. + /// A function to extract a key from the AVObject. + /// A new AVQuery based on Source whose results will be ordered by + /// the key specified in the keySelector. + public static AVQuery ThenByDescending( + this AVQuery source, Expression> keySelector) + where TSource : AVObject + { + return source.ThenByDescending(GetOrderByPath(keySelector)); + } + + /// + /// Correlates the elements of two queries based on matching keys. + /// + /// The type of AVObjects of the first query. + /// The type of AVObjects of the second query. + /// The type of the keys returned by the key selector + /// functions. + /// The type of the result. This must match either + /// TOuter or TInner + /// The first query to join. + /// The query to join to the first query. + /// A function to extract a join key from the results of + /// the first query. + /// A function to extract a join key from the results of + /// the second query. + /// A function to select either the outer or inner query + /// result to determine which query is the base query. + /// A new AVQuery with a WhereMatchesQuery or WhereMatchesKeyInQuery + /// clause based upon the query indicated in the . + public static AVQuery Join( + this AVQuery outer, + AVQuery inner, + Expression> outerKeySelector, + Expression> innerKeySelector, + Expression> resultSelector) + where TOuter : AVObject + where TInner : AVObject + where TResult : AVObject + { + // resultSelector must select either the inner object or the outer object. If it's the inner + // object, reverse the query. + if (resultSelector.Body == resultSelector.Parameters[1]) + { + // The inner object was selected. + return inner.Join( + outer, + innerKeySelector, + outerKeySelector, + (i, o) => i) as AVQuery; + } + if (resultSelector.Body != resultSelector.Parameters[0]) + { + throw new InvalidOperationException("Joins must select either the outer or inner object."); + } + + // Normalize both selectors + Expression outerNormalized = new ObjectNormalizer().Visit(outerKeySelector.Body); + Expression innerNormalized = new ObjectNormalizer().Visit(innerKeySelector.Body); + MethodCallExpression outerAsGet = outerNormalized as MethodCallExpression; + MethodCallExpression innerAsGet = innerNormalized as MethodCallExpression; + if (IsAVObjectGet(outerAsGet) && outerAsGet.Object == outerKeySelector.Parameters[0]) + { + var outerKey = GetValue(outerAsGet.Arguments[0]) as string; + + if (IsAVObjectGet(innerAsGet) && innerAsGet.Object == innerKeySelector.Parameters[0]) + { + // Both are key accesses, so treat this as a WhereMatchesKeyInQuery + var innerKey = GetValue(innerAsGet.Arguments[0]) as string; + return outer.WhereMatchesKeyInQuery(outerKey, innerKey, inner) as AVQuery; + } + + if (innerKeySelector.Body == innerKeySelector.Parameters[0]) + { + // The inner selector is on the result of the query itself, so treat this as a + // WhereMatchesQuery + return outer.WhereMatchesQuery(outerKey, inner) as AVQuery; + } + throw new InvalidOperationException( + "The key for the joined object must be a AVObject or a field access " + + "on the AVObject."); + } + + // TODO (hallucinogen): If we ever support "and" queries fully and/or support a "where this object + // matches some key in some other query" (as opposed to requiring a key on this query), we + // can add support for even more types of joins. + + throw new InvalidOperationException( + "The key for the selected object must be a field access on the AVObject."); + } + } +} diff --git a/Storage/Source/Public/AVRelation.cs b/Storage/Source/Public/AVRelation.cs new file mode 100644 index 0000000..fa7f13e --- /dev/null +++ b/Storage/Source/Public/AVRelation.cs @@ -0,0 +1,175 @@ +using LeanCloud.Storage.Internal; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; + +namespace LeanCloud +{ + /// + /// A common base class for AVRelations. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public abstract class AVRelationBase : IJsonConvertible + { + private AVObject parent; + private string key; + private string targetClassName; + + internal AVRelationBase(AVObject parent, string key) + { + EnsureParentAndKey(parent, key); + } + + internal AVRelationBase(AVObject parent, string key, string targetClassName) + : this(parent, key) + { + this.targetClassName = targetClassName; + } + + internal static IObjectSubclassingController SubclassingController + { + get + { + return AVPlugins.Instance.SubclassingController; + } + } + + internal void EnsureParentAndKey(AVObject parent, string key) + { + this.parent = this.parent ?? parent; + this.key = this.key ?? key; + Debug.Assert(this.parent == parent, "Relation retrieved from two different objects"); + Debug.Assert(this.key == key, "Relation retrieved from two different keys"); + } + + internal void Add(AVObject obj) + { + var change = new AVRelationOperation(new[] { obj }, null); + parent.PerformOperation(key, change); + targetClassName = change.TargetClassName; + } + + internal void Remove(AVObject obj) + { + var change = new AVRelationOperation(null, new[] { obj }); + parent.PerformOperation(key, change); + targetClassName = change.TargetClassName; + } + + IDictionary IJsonConvertible.ToJSON() + { + return new Dictionary { + { "__type", "Relation"}, + { "className", targetClassName} + }; + } + + internal AVQuery GetQuery() where T : AVObject + { + if (targetClassName != null) + { + return new AVQuery(targetClassName) + .WhereRelatedTo(parent, key); + } + + return new AVQuery(parent.ClassName) + .RedirectClassName(key) + .WhereRelatedTo(parent, key); + } + + internal AVQuery GetReverseQuery(T target) where T : AVObject + { + if (target.ObjectId == null) + { + throw new ArgumentNullException("target.ObjectId", "can not query a relation without target ObjectId."); + } + + return new AVQuery(parent.ClassName).WhereEqualTo(key, target); + } + + internal string TargetClassName + { + get + { + return targetClassName; + } + set + { + targetClassName = value; + } + } + + /// + /// Produces the proper AVRelation<T> instance for the given classname. + /// + internal static AVRelationBase CreateRelation(AVObject parent, + string key, + string targetClassName) + { + var targetType = SubclassingController.GetType(targetClassName) ?? typeof(AVObject); + + Expression>> createRelationExpr = + () => CreateRelation(parent, key, targetClassName); + var createRelationMethod = + ((MethodCallExpression)createRelationExpr.Body) + .Method + .GetGenericMethodDefinition() + .MakeGenericMethod(targetType); + return (AVRelationBase)createRelationMethod.Invoke(null, new object[] { parent, key, targetClassName }); + } + + private static AVRelation CreateRelation(AVObject parent, string key, string targetClassName) + where T : AVObject + { + return new AVRelation(parent, key, targetClassName); + } + } + + /// + /// Provides access to all of the children of a many-to-many relationship. Each instance of + /// AVRelation is associated with a particular parent and key. + /// + /// The type of the child objects. + public sealed class AVRelation : AVRelationBase where T : AVObject + { + + internal AVRelation(AVObject parent, string key) : base(parent, key) { } + + internal AVRelation(AVObject parent, string key, string targetClassName) + : base(parent, key, targetClassName) { } + + /// + /// Adds an object to this relation. The object must already have been saved. + /// + /// The object to add. + public void Add(T obj) + { + base.Add(obj); + } + + /// + /// Removes an object from this relation. The object must already have been saved. + /// + /// The object to remove. + public void Remove(T obj) + { + base.Remove(obj); + } + + /// + /// Gets a query that can be used to query the objects in this relation. + /// + public AVQuery Query + { + get + { + return base.GetQuery(); + } + } + } +} diff --git a/Storage/Source/Public/AVRole.cs b/Storage/Source/Public/AVRole.cs new file mode 100644 index 0000000..848832b --- /dev/null +++ b/Storage/Source/Public/AVRole.cs @@ -0,0 +1,111 @@ +using LeanCloud.Storage.Internal; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace LeanCloud +{ + /// + /// Represents a Role on the LeanCloud server. AVRoles represent groupings + /// of s for the purposes of granting permissions (e.g. + /// specifying a for a . Roles + /// are specified by their sets of child users and child roles, all of which are granted + /// any permissions that the parent role has. + /// + /// Roles must have a name (that cannot be changed after creation of the role), + /// and must specify an ACL. + /// + [AVClassName("_Role")] + public class AVRole : AVObject + { + private static readonly Regex namePattern = new Regex("^[0-9a-zA-Z_\\- ]+$"); + + /// + /// Constructs a new AVRole. You must assign a name and ACL to the role. + /// + public AVRole() : base() { } + + /// + /// Constructs a new AVRole with the given name. + /// + /// The name of the role to create. + /// The ACL for this role. Roles must have an ACL. + public AVRole(string name, AVACL acl) + : this() + { + Name = name; + ACL = acl; + } + + /// + /// Gets the name of the role. + /// + [AVFieldName("name")] + public string Name + { + get { return GetProperty("Name"); } + set { SetProperty(value, "Name"); } + } + + /// + /// Gets the for the s that are + /// direct children of this role. These users are granted any privileges that + /// this role has been granted (e.g. read or write access through ACLs). You can + /// add or remove child users from the role through this relation. + /// + [AVFieldName("users")] + public AVRelation Users + { + get { return GetRelationProperty("Users"); } + } + + /// + /// Gets the for the s that are + /// direct children of this role. These roles' users are granted any privileges that + /// this role has been granted (e.g. read or write access through ACLs). You can + /// add or remove child roles from the role through this relation. + /// + [AVFieldName("roles")] + public AVRelation Roles + { + get { return GetRelationProperty("Roles"); } + } + + internal override void OnSettingValue(ref string key, ref object value) + { + base.OnSettingValue(ref key, ref value); + if (key == "name") + { + if (ObjectId != null) + { + throw new InvalidOperationException( + "A role's name can only be set before it has been saved."); + } + if (!(value is string)) + { + throw new ArgumentException("A role's name must be a string.", "value"); + } + if (!namePattern.IsMatch((string)value)) + { + throw new ArgumentException( + "A role's name can only contain alphanumeric characters, _, -, and spaces.", + "value"); + } + } + } + + /// + /// Gets a over the Role collection. + /// + public static AVQuery Query + { + get + { + return new AVQuery(); + } + } + } +} diff --git a/Storage/Source/Public/AVSession.cs b/Storage/Source/Public/AVSession.cs new file mode 100644 index 0000000..e915b40 --- /dev/null +++ b/Storage/Source/Public/AVSession.cs @@ -0,0 +1,111 @@ +using LeanCloud.Storage.Internal; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace LeanCloud +{ + /// + /// Represents a session of a user for a LeanCloud application. + /// + [AVClassName("_Session")] + public class AVSession : AVObject + { + private static readonly HashSet readOnlyKeys = new HashSet { + "sessionToken", "createdWith", "restricted", "user", "expiresAt", "installationId" + }; + + protected override bool IsKeyMutable(string key) + { + return !readOnlyKeys.Contains(key); + } + + /// + /// Gets the session token for a user, if they are logged in. + /// + [AVFieldName("sessionToken")] + public string SessionToken + { + get { return GetProperty(null, "SessionToken"); } + } + + /// + /// Constructs a for AVSession. + /// + public static AVQuery Query + { + get + { + return new AVQuery(); + } + } + + internal static IAVSessionController SessionController + { + get + { + return AVPlugins.Instance.SessionController; + } + } + + /// + /// Gets the current object related to the current user. + /// + public static Task GetCurrentSessionAsync() + { + return GetCurrentSessionAsync(CancellationToken.None); + } + + /// + /// Gets the current object related to the current user. + /// + /// The cancellation token + public static Task GetCurrentSessionAsync(CancellationToken cancellationToken) + { + return AVUser.GetCurrentUserAsync().OnSuccess(t1 => + { + AVUser user = t1.Result; + if (user == null) + { + return Task.FromResult((AVSession)null); + } + + string sessionToken = user.SessionToken; + if (sessionToken == null) + { + return Task.FromResult((AVSession)null); + } + + return SessionController.GetSessionAsync(sessionToken, cancellationToken).OnSuccess(t => + { + AVSession session = AVObject.FromState(t.Result, "_Session"); + return session; + }); + }).Unwrap(); + } + + internal static Task RevokeAsync(string sessionToken, CancellationToken cancellationToken) + { + if (sessionToken == null || !SessionController.IsRevocableSessionToken(sessionToken)) + { + return Task.FromResult(0); + } + return SessionController.RevokeAsync(sessionToken, cancellationToken); + } + + internal static Task UpgradeToRevocableSessionAsync(string sessionToken, CancellationToken cancellationToken) + { + if (sessionToken == null || SessionController.IsRevocableSessionToken(sessionToken)) + { + return Task.FromResult(sessionToken); + } + + return SessionController.UpgradeToRevocableSessionAsync(sessionToken, cancellationToken).OnSuccess(t => + { + AVSession session = AVObject.FromState(t.Result, "_Session"); + return session.SessionToken; + }); + } + } +} diff --git a/Storage/Source/Public/AVStatus.cs b/Storage/Source/Public/AVStatus.cs new file mode 100644 index 0000000..7a7bc65 --- /dev/null +++ b/Storage/Source/Public/AVStatus.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LeanCloud +{ + /// + /// 事件流系统中的一条状态 + /// + [AVClassName("_Status")] + public class AVStatus : AVObject + { + private static readonly HashSet readOnlyKeys = new HashSet { + "messageId", "inboxType", "data","Source" + }; + + protected override bool IsKeyMutable(string key) + { + return !readOnlyKeys.Contains(key); + } + } +} diff --git a/Storage/Source/Public/AVUploadProgressEventArgs.cs b/Storage/Source/Public/AVUploadProgressEventArgs.cs new file mode 100644 index 0000000..dfa3112 --- /dev/null +++ b/Storage/Source/Public/AVUploadProgressEventArgs.cs @@ -0,0 +1,15 @@ +using System; + +namespace LeanCloud { + /// + /// Represents upload progress. + /// + public class AVUploadProgressEventArgs : EventArgs { + public AVUploadProgressEventArgs() { } + + /// + /// Gets the progress (a number between 0.0 and 1.0) of an upload. + /// + public double Progress { get; set; } + } +} diff --git a/Storage/Source/Public/AVUser.cs b/Storage/Source/Public/AVUser.cs new file mode 100644 index 0000000..62aee39 --- /dev/null +++ b/Storage/Source/Public/AVUser.cs @@ -0,0 +1,1544 @@ +using LeanCloud.Storage.Internal; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace LeanCloud +{ + /// + /// Represents a user for a LeanCloud application. + /// + [AVClassName("_User")] + public class AVUser : AVObject + { + private static readonly IDictionary authProviders = + new Dictionary(); + + private static readonly HashSet readOnlyKeys = new HashSet { + "sessionToken", "isNew" + }; + + internal static IAVUserController UserController + { + get + { + return AVPlugins.Instance.UserController; + } + } + + internal static IAVCurrentUserController CurrentUserController + { + get + { + return AVPlugins.Instance.CurrentUserController; + } + } + + /// + /// Whether the AVUser has been authenticated on this device. Only an authenticated + /// AVUser can be saved and deleted. + /// + [Obsolete("This property is deprecated, please use IsAuthenticatedAsync instead.")] + public bool IsAuthenticated + { + get + { + lock (mutex) + { + return SessionToken != null && + CurrentUser != null && + CurrentUser.ObjectId == ObjectId; + } + } + } + + /// + /// Whether the AVUser has been authenticated on this device, and the AVUser's session token is expired. + /// Only an authenticated AVUser can be saved and deleted. + /// + public Task IsAuthenticatedAsync() + { + lock (mutex) + { + if (SessionToken == null || CurrentUser == null || CurrentUser.ObjectId != ObjectId) + { + return Task.FromResult(false); + } + } + var command = new AVCommand(String.Format("users/me?session_token={0}", CurrentSessionToken), + method: "GET", + data: null); + return AVPlugins.Instance.CommandRunner.RunCommandAsync(command).ContinueWith(t => + { + return AVClient.IsSuccessStatusCode(t.Result.Item1); + }); + } + + /// + /// Refresh this user's session token, and current session token will be invalid. + /// + public Task RefreshSessionTokenAsync(CancellationToken cancellationToken) + { + return UserController.RefreshSessionTokenAsync(ObjectId, SessionToken, cancellationToken).OnSuccess(t => + { + var serverState = t.Result; + HandleSave(serverState); + }); + } + + /// + /// Removes a key from the object's data if it exists. + /// + /// The key to remove. + /// Cannot remove the username key. + public override void Remove(string key) + { + if (key == "username") + { + throw new ArgumentException("Cannot remove the username key."); + } + base.Remove(key); + } + + protected override bool IsKeyMutable(string key) + { + return !readOnlyKeys.Contains(key); + } + + internal override void HandleSave(IObjectState serverState) + { + base.HandleSave(serverState); + + SynchronizeAllAuthData(); + CleanupAuthData(); + + MutateState(mutableClone => + { + mutableClone.ServerData.Remove("password"); + }); + } + + /// + /// authenticated token. + /// + public string SessionToken + { + get + { + if (State.ContainsKey("sessionToken")) + { + return State["sessionToken"] as string; + } + return null; + } + } + + internal static string CurrentSessionToken + { + get + { + Task sessionTokenTask = GetCurrentSessionTokenAsync(); + sessionTokenTask.Wait(); + return sessionTokenTask.Result; + } + } + + internal static Task GetCurrentSessionTokenAsync(CancellationToken cancellationToken = default(CancellationToken)) + { + return CurrentUserController.GetCurrentSessionTokenAsync(cancellationToken); + } + + internal Task SetSessionTokenAsync(string newSessionToken) + { + return SetSessionTokenAsync(newSessionToken, CancellationToken.None); + } + + internal Task SetSessionTokenAsync(string newSessionToken, CancellationToken cancellationToken) + { + MutateState(mutableClone => + { + mutableClone.ServerData["sessionToken"] = newSessionToken; + }); + + return SaveCurrentUserAsync(this); + } + + /// + /// Gets or sets the username. + /// + [AVFieldName("username")] + public string Username + { + get { return GetProperty(null, "Username"); } + set { SetProperty(value, "Username"); } + } + + /// + /// Sets the password. + /// + [AVFieldName("password")] + public string Password + { + private get { return GetProperty(null, "Password"); } + set { SetProperty(value, "Password"); } + } + + /// + /// Sets the email address. + /// + [AVFieldName("email")] + public string Email + { + get { return GetProperty(null, "Email"); } + set { SetProperty(value, "Email"); } + } + + /// + /// 用户手机号。 + /// + [AVFieldName("mobilePhoneNumber")] + public string MobilePhoneNumber + { + get + { + return GetProperty(null, "MobilePhoneNumber"); + } + set + { + SetProperty(value, "MobilePhoneNumber"); + } + } + + /// + /// 用户手机号是否已经验证 + /// + /// true if mobile phone verified; otherwise, false. + [AVFieldName("mobilePhoneVerified")] + public bool MobilePhoneVerified + { + get + { + return GetProperty(false, "MobilePhoneVerified"); + } + set + { + SetProperty(value, "MobilePhoneVerified"); + } + } + + /// + /// 判断用户是否为匿名用户 + /// + public bool IsAnonymous + { + get + { + bool rtn = false; + if (this.AuthData != null) + { + rtn = this.AuthData.Keys.Contains("anonymous"); + } + return rtn; + } + } + + internal Task SignUpAsync(Task toAwait, CancellationToken cancellationToken) + { + return this.Create(toAwait, cancellationToken).OnSuccess(_ => SaveCurrentUserAsync(this)).Unwrap(); + } + + /// + /// Signs up a new user. This will create a new AVUser on the server and will also persist the + /// session on disk so that you can access the user using . A username and + /// password must be set before calling SignUpAsync. + /// + public Task SignUpAsync() + { + return SignUpAsync(CancellationToken.None); + } + + /// + /// Signs up a new user. This will create a new AVUser on the server and will also persist the + /// session on disk so that you can access the user using . A username and + /// password must be set before calling SignUpAsync. + /// + /// The cancellation token. + public Task SignUpAsync(CancellationToken cancellationToken) + { + return taskQueue.Enqueue(toAwait => SignUpAsync(toAwait, cancellationToken), + cancellationToken); + } + + #region 事件流系统相关 API + + /// + /// 关注某个用户 + /// + /// 被关注的用户 + /// + public Task FollowAsync(string userObjectId) + { + return this.FollowAsync(userObjectId, null); + } + + /// + /// 关注某个用户 + /// + /// 被关注的用户Id + /// 关注的时候附加属性 + /// + public Task FollowAsync(string userObjectId, IDictionary data) + { + if (data != null) + { + data = this.EncodeForSaving(data); + } + var command = new AVCommand(string.Format("users/{0}/friendship/{1}", this.ObjectId, userObjectId), + method: "POST", + sessionToken: CurrentSessionToken, + data: data); + return AVPlugins.Instance.CommandRunner.RunCommandAsync(command).ContinueWith(t => + { + return AVClient.IsSuccessStatusCode(t.Result.Item1); + }); + } + + /// + /// 取关某一个用户 + /// + /// + /// + public Task UnfollowAsync(string userObjectId) + { + var command = new AVCommand(string.Format("users/{0}/friendship/{1}", this.ObjectId, userObjectId), + method: "DELETE", + sessionToken: CurrentSessionToken, + data: null); + return AVPlugins.Instance.CommandRunner.RunCommandAsync(command).ContinueWith(t => + { + return AVClient.IsSuccessStatusCode(t.Result.Item1); + }); + } + + /// + /// 获取当前用户的关注者的查询 + /// + /// + public AVQuery GetFollowerQuery() + { + AVQuery query = new AVQuery(); + query.RelativeUri = string.Format("users/{0}/followers", this.ObjectId); + return query; + } + + /// + /// 获取当前用户所关注的用户的查询 + /// + /// + public AVQuery GetFolloweeQuery() + { + AVQuery query = new AVQuery(); + query.RelativeUri = string.Format("users/{0}/followees", this.ObjectId); + return query; + } + + /// + /// 同时查询关注了当前用户的关注者和当前用户所关注的用户 + /// + /// + public AVQuery GetFollowersAndFolloweesQuery() + { + AVQuery query = new AVQuery(); + query.RelativeUri = string.Format("users/{0}/followersAndFollowees", this.ObjectId); + return query; + } + + /// + /// 获取当前用户的关注者 + /// + /// + public Task> GetFollowersAsync() + { + return this.GetFollowerQuery().FindAsync(); + } + + /// + /// 获取当前用户所关注的用户 + /// + /// + public Task> GetFolloweesAsync() + { + return this.GetFolloweeQuery().FindAsync(); + } + + + //public Task SendStatusAsync() + //{ + + //} + + #endregion + + /// + /// Logs in a user with a username and password. On success, this saves the session to disk so you + /// can retrieve the currently logged in user using . + /// + /// The username to log in with. + /// The password to log in with. + /// The newly logged-in user. + public static Task LogInAsync(string username, string password) + { + return LogInAsync(username, password, CancellationToken.None); + } + + /// + /// Logs in a user with a username and password. On success, this saves the session to disk so you + /// can retrieve the currently logged in user using . + /// + /// The username to log in with. + /// The password to log in with. + /// The cancellation token. + /// The newly logged-in user. + public static Task LogInAsync(string username, + string password, + CancellationToken cancellationToken) + { + return UserController.LogInAsync(username, null, password, cancellationToken).OnSuccess(t => + { + AVUser user = AVObject.FromState(t.Result, "_User"); + return SaveCurrentUserAsync(user).OnSuccess(_ => user); + }).Unwrap(); + } + + /// + /// Logs in a user with a username and password. On success, this saves the session to disk so you + /// can retrieve the currently logged in user using . + /// + /// The session token to authorize with + /// The user if authorization was successful + public static Task BecomeAsync(string sessionToken) + { + return BecomeAsync(sessionToken, CancellationToken.None); + } + + /// + /// Logs in a user with a username and password. On success, this saves the session to disk so you + /// can retrieve the currently logged in user using . + /// + /// The session token to authorize with + /// The cancellation token. + /// The user if authorization was successful + public static Task BecomeAsync(string sessionToken, CancellationToken cancellationToken) + { + return UserController.GetUserAsync(sessionToken, cancellationToken).OnSuccess(t => + { + AVUser user = AVObject.FromState(t.Result, "_User"); + return SaveCurrentUserAsync(user).OnSuccess(_ => user); + }).Unwrap(); + } + + protected override Task SaveAsync(Task toAwait, CancellationToken cancellationToken) + { + lock (mutex) + { + if (ObjectId == null) + { + throw new InvalidOperationException("You must call SignUpAsync before calling SaveAsync."); + } + return base.SaveAsync(toAwait, cancellationToken).OnSuccess(_ => + { + if (!CurrentUserController.IsCurrent(this)) + { + return Task.FromResult(0); + } + return SaveCurrentUserAsync(this); + }).Unwrap(); + } + } + + internal override Task FetchAsyncInternal(Task toAwait, IDictionary queryString, CancellationToken cancellationToken) + { + return base.FetchAsyncInternal(toAwait, queryString, cancellationToken).OnSuccess(t => + { + if (!CurrentUserController.IsCurrent(this)) + { + return Task.FromResult(t.Result); + } + // If this is already the current user, refresh its state on disk. + return SaveCurrentUserAsync(this).OnSuccess(_ => t.Result); + }).Unwrap(); + } + + /// + /// Logs out the currently logged in user session. This will remove the session from disk, log out of + /// linked services, and future calls to will return null. + /// + /// + /// Typically, you should use , unless you are managing your own threading. + /// + public static void LogOut() + { + // TODO (hallucinogen): this will without a doubt fail in Unity. But what else can we do? + LogOutAsync().Wait(); + } + + /// + /// Logs out the currently logged in user session. This will remove the session from disk, log out of + /// linked services, and future calls to will return null. + /// + /// + /// This is preferable to using , unless your code is already running from a + /// background thread. + /// + public static Task LogOutAsync() + { + return LogOutAsync(CancellationToken.None); + } + + /// + /// Logs out the currently logged in user session. This will remove the session from disk, log out of + /// linked services, and future calls to will return null. + /// + /// This is preferable to using , unless your code is already running from a + /// background thread. + /// + public static Task LogOutAsync(CancellationToken cancellationToken) + { + return GetCurrentUserAsync().OnSuccess(t => + { + LogOutWithProviders(); + + AVUser user = t.Result; + if (user == null) + { + return Task.FromResult(0); + } + + return user.taskQueue.Enqueue(toAwait => user.LogOutAsync(toAwait, cancellationToken), cancellationToken); + }).Unwrap(); + } + + internal Task LogOutAsync(Task toAwait, CancellationToken cancellationToken) + { + string oldSessionToken = SessionToken; + if (oldSessionToken == null) + { + return Task.FromResult(0); + } + + // Cleanup in-memory session. + MutateState(mutableClone => + { + mutableClone.ServerData.Remove("sessionToken"); + }); + var revokeSessionTask = AVSession.RevokeAsync(oldSessionToken, cancellationToken); + return Task.WhenAll(revokeSessionTask, CurrentUserController.LogOutAsync(cancellationToken)); + } + + private static void LogOutWithProviders() + { + foreach (var provider in authProviders.Values) + { + provider.Deauthenticate(); + } + } + + /// + /// Gets the currently logged in AVUser with a valid session, either from memory or disk + /// if necessary. + /// + public static AVUser CurrentUser + { + get + { + var userTask = GetCurrentUserAsync(); + // TODO (hallucinogen): this will without a doubt fail in Unity. How should we fix it? + userTask.Wait(); + return userTask.Result; + } + } + + public static Task GetCurrentAsync() + { + var userTask = GetCurrentUserAsync(); + return userTask; + } + + /// + /// Gets the currently logged in AVUser with a valid session, either from memory or disk + /// if necessary, asynchronously. + /// + public static Task GetCurrentUserAsync() + { + return GetCurrentUserAsync(CancellationToken.None); + } + + /// + /// Gets the currently logged in AVUser with a valid session, either from memory or disk + /// if necessary, asynchronously. + /// + internal static Task GetCurrentUserAsync(CancellationToken cancellationToken) + { + return CurrentUserController.GetAsync(cancellationToken); + } + + private static Task SaveCurrentUserAsync(AVUser user) + { + return SaveCurrentUserAsync(user, CancellationToken.None); + } + + private static Task SaveCurrentUserAsync(AVUser user, CancellationToken cancellationToken) + { + return CurrentUserController.SetAsync(user, cancellationToken); + } + + internal static void ClearInMemoryUser() + { + CurrentUserController.ClearFromMemory(); + } + + /// + /// Constructs a for AVUsers. + /// + public static AVQuery Query + { + get + { + return new AVQuery(); + } + } + + #region Legacy / Revocable Session Tokens + + private static readonly object isRevocableSessionEnabledMutex = new object(); + private static bool isRevocableSessionEnabled; + + /// + /// Tells server to use revocable session on LogIn and SignUp, even when App's Settings + /// has "Require Revocable Session" turned off. Issues network request in background to + /// migrate the sessionToken on disk to revocable session. + /// + /// The Task that upgrades the session. + public static Task EnableRevocableSessionAsync() + { + return EnableRevocableSessionAsync(CancellationToken.None); + } + + /// + /// Tells server to use revocable session on LogIn and SignUp, even when App's Settings + /// has "Require Revocable Session" turned off. Issues network request in background to + /// migrate the sessionToken on disk to revocable session. + /// + /// The Task that upgrades the session. + public static Task EnableRevocableSessionAsync(CancellationToken cancellationToken) + { + lock (isRevocableSessionEnabledMutex) + { + isRevocableSessionEnabled = true; + } + + return GetCurrentUserAsync(cancellationToken).OnSuccess(t => + { + var user = t.Result; + return user.UpgradeToRevocableSessionAsync(cancellationToken); + }); + } + + internal static void DisableRevocableSession() + { + lock (isRevocableSessionEnabledMutex) + { + isRevocableSessionEnabled = false; + } + } + + internal static bool IsRevocableSessionEnabled + { + get + { + lock (isRevocableSessionEnabledMutex) + { + return isRevocableSessionEnabled; + } + } + } + + internal Task UpgradeToRevocableSessionAsync() + { + return UpgradeToRevocableSessionAsync(CancellationToken.None); + } + + public Task UpgradeToRevocableSessionAsync(CancellationToken cancellationToken) + { + return taskQueue.Enqueue(toAwait => UpgradeToRevocableSessionAsync(toAwait, cancellationToken), + cancellationToken); + } + + internal Task UpgradeToRevocableSessionAsync(Task toAwait, CancellationToken cancellationToken) + { + string sessionToken = SessionToken; + + return toAwait.OnSuccess(_ => + { + return AVSession.UpgradeToRevocableSessionAsync(sessionToken, cancellationToken); + }).Unwrap().OnSuccess(t => + { + return SetSessionTokenAsync(t.Result); + }).Unwrap(); + } + + #endregion + + /// + /// Requests a password reset email to be sent to the specified email address associated with the + /// user account. This email allows the user to securely reset their password on the LeanCloud site. + /// + /// The email address associated with the user that forgot their password. + public static Task RequestPasswordResetAsync(string email) + { + return RequestPasswordResetAsync(email, CancellationToken.None); + } + + /// + /// Requests a password reset email to be sent to the specified email address associated with the + /// user account. This email allows the user to securely reset their password on the LeanCloud site. + /// + /// The email address associated with the user that forgot their password. + /// The cancellation token. + public static Task RequestPasswordResetAsync(string email, + CancellationToken cancellationToken) + { + return UserController.RequestPasswordResetAsync(email, cancellationToken); + } + + /// + /// Updates current user's password. Need the user's old password, + /// + /// The password. + /// Old password. + /// New password. + /// Cancellation token. + public Task UpdatePassword(string oldPassword, string newPassword, CancellationToken cancellationToken) + { + return UserController.UpdatePasswordAsync(ObjectId, SessionToken, oldPassword, newPassword, cancellationToken); + } + + /// + /// Gets the authData for this user. + /// + internal IDictionary> AuthData + { + get + { + IDictionary> authData; + if (this.TryGetValue>>( + "authData", out authData)) + { + return authData; + } + return null; + } + private set + { + this["authData"] = value; + } + } + + private static IAVAuthenticationProvider GetProvider(string providerName) + { + IAVAuthenticationProvider provider; + if (authProviders.TryGetValue(providerName, out provider)) + { + return provider; + } + return null; + } + + /// + /// Removes null values from authData (which exist temporarily for unlinking) + /// + private void CleanupAuthData() + { + lock (mutex) + { + if (!CurrentUserController.IsCurrent(this)) + { + return; + } + var authData = AuthData; + + if (authData == null) + { + return; + } + + foreach (var pair in new Dictionary>(authData)) + { + if (pair.Value == null) + { + authData.Remove(pair.Key); + } + } + } + } + + /// + /// Synchronizes authData for all providers. + /// + private void SynchronizeAllAuthData() + { + lock (mutex) + { + var authData = AuthData; + + if (authData == null) + { + return; + } + + foreach (var pair in authData) + { + SynchronizeAuthData(GetProvider(pair.Key)); + } + } + } + + private void SynchronizeAuthData(IAVAuthenticationProvider provider) + { + bool restorationSuccess = false; + lock (mutex) + { + var authData = AuthData; + if (authData == null || provider == null) + { + return; + } + IDictionary data; + if (authData.TryGetValue(provider.AuthType, out data)) + { + restorationSuccess = provider.RestoreAuthentication(data); + } + } + + if (!restorationSuccess) + { + this.UnlinkFromAsync(provider.AuthType, CancellationToken.None); + } + } + + public Task LinkWithAsync(string authType, IDictionary data, CancellationToken cancellationToken) + { + return taskQueue.Enqueue(toAwait => + { + AuthData = new Dictionary>(); + AuthData[authType] = data; + return SaveAsync(cancellationToken); + }, cancellationToken); + } + + public Task LinkWithAsync(string authType, CancellationToken cancellationToken) + { + var provider = GetProvider(authType); + return provider.AuthenticateAsync(cancellationToken) + .OnSuccess(t => LinkWithAsync(authType, t.Result, cancellationToken)) + .Unwrap(); + } + + /// + /// Unlinks a user from a service. + /// + public Task UnlinkFromAsync(string authType, CancellationToken cancellationToken) + { + return LinkWithAsync(authType, null, cancellationToken); + } + + /// + /// Checks whether a user is linked to a service. + /// + internal bool IsLinked(string authType) + { + lock (mutex) + { + return AuthData != null && AuthData.ContainsKey(authType) && AuthData[authType] != null; + } + } + + internal static Task LogInWithAsync(string authType, + IDictionary data, + bool failOnNotExist, + CancellationToken cancellationToken) + { + AVUser user = null; + + return UserController.LogInAsync(authType, data, failOnNotExist, cancellationToken).OnSuccess(t => + { + user = AVObject.FromState(t.Result, "_User"); + + lock (user.mutex) + { + if (user.AuthData == null) + { + user.AuthData = new Dictionary>(); + } + user.AuthData[authType] = data; + user.SynchronizeAllAuthData(); + } + + return SaveCurrentUserAsync(user); + }).Unwrap().OnSuccess(t => user); + } + + internal static Task LogInWithAsync(string authType, + CancellationToken cancellationToken) + { + var provider = GetProvider(authType); + return provider.AuthenticateAsync(cancellationToken) + .OnSuccess(authData => LogInWithAsync(authType, authData.Result, false, cancellationToken)) + .Unwrap(); + } + + internal static void RegisterProvider(IAVAuthenticationProvider provider) + { + authProviders[provider.AuthType] = provider; + var curUser = AVUser.CurrentUser; + if (curUser != null) + { + curUser.SynchronizeAuthData(provider); + } + } + + #region 手机号登录 + + internal static Task LogInWithParametersAsync(Dictionary strs, CancellationToken cancellationToken) + { + AVUser avUser = AVObject.CreateWithoutData(null); + + return UserController.LogInWithParametersAsync("login", strs, cancellationToken).OnSuccess(t => + { + var user = (AVUser)AVObject.CreateWithoutData(null); + user.HandleFetchResult(t.Result); + return SaveCurrentUserAsync(user).OnSuccess(_ => user); + }).Unwrap(); + } + + /// + /// 以手机号和密码实现登陆。 + /// + /// 手机号 + /// 密码 + /// + public static Task LogInByMobilePhoneNumberAsync(string mobilePhoneNumber, string password) + { + return AVUser.LogInByMobilePhoneNumberAsync(mobilePhoneNumber, password, CancellationToken.None); + } + + /// + /// 以手机号和验证码匹配登陆 + /// + /// 手机号 + /// 短信验证码 + /// + public static Task LogInBySmsCodeAsync(string mobilePhoneNumber, string smsCode) + { + return AVUser.LogInBySmsCodeAsync(mobilePhoneNumber, smsCode, CancellationToken.None); + } + + /// + /// 用邮箱作和密码匹配登录 + /// + /// 邮箱 + /// 密码 + /// + public static Task LogInByEmailAsync(string email, string password, CancellationToken cancellationToken = default(CancellationToken)) + { + return UserController.LogInAsync(null, email, password, cancellationToken).OnSuccess(t => { + AVUser user = AVObject.FromState(t.Result, "_User"); + return SaveCurrentUserAsync(user).OnSuccess(_ => user); + }).Unwrap(); + } + + + /// + /// 以手机号和密码匹配登陆 + /// + /// 手机号 + /// 密码 + /// + /// + public static Task LogInByMobilePhoneNumberAsync(string mobilePhoneNumber, string password, CancellationToken cancellationToken) + { + Dictionary strs = new Dictionary() + { + { "mobilePhoneNumber", mobilePhoneNumber }, + { "password", password } + }; + return AVUser.LogInWithParametersAsync(strs, cancellationToken); + } + + /// + /// 以手机号和验证码登陆 + /// + /// 手机号 + /// 短信验证码 + /// + /// + public static Task LogInBySmsCodeAsync(string mobilePhoneNumber, string smsCode, CancellationToken cancellationToken = default(CancellationToken)) + { + Dictionary strs = new Dictionary() + { + { "mobilePhoneNumber", mobilePhoneNumber }, + { "smsCode", smsCode } + }; + return AVUser.LogInWithParametersAsync(strs, cancellationToken); + } + + /// + /// Requests the login SMS code asynchronous. + /// + /// The mobile phone number. + /// + public static Task RequestLogInSmsCodeAsync(string mobilePhoneNumber) + { + return AVUser.RequestLogInSmsCodeAsync(mobilePhoneNumber, CancellationToken.None); + } + + /// + /// Requests the login SMS code asynchronous. + /// + /// The mobile phone number. + /// Validate token. + /// + public static Task RequestLogInSmsCodeAsync(string mobilePhoneNumber, string validateToken) + { + return AVUser.RequestLogInSmsCodeAsync(mobilePhoneNumber, null, CancellationToken.None); + } + + /// + /// Requests the login SMS code asynchronous. + /// + /// The mobile phone number. + /// The cancellation token. + /// + public static Task RequestLogInSmsCodeAsync(string mobilePhoneNumber, CancellationToken cancellationToken) + { + return RequestLogInSmsCodeAsync(mobilePhoneNumber, null, cancellationToken); + } + + /// + /// Requests the login SMS code asynchronous. + /// + /// The mobile phone number. + /// Validate token. + /// The cancellation token. + /// + public static Task RequestLogInSmsCodeAsync(string mobilePhoneNumber, string validateToken, CancellationToken cancellationToken) + { + Dictionary strs = new Dictionary() + { + { "mobilePhoneNumber", mobilePhoneNumber }, + }; + if (String.IsNullOrEmpty(validateToken)) + { + strs.Add("validate_token", validateToken); + } + var command = new AVCommand("requestLoginSmsCode", + method: "POST", + sessionToken: CurrentSessionToken, + data: strs); + return AVPlugins.Instance.CommandRunner.RunCommandAsync(command).ContinueWith(t => + { + return AVClient.IsSuccessStatusCode(t.Result.Item1); + }); + } + + /// + /// 手机号一键登录 + /// + /// 手机号 + /// 短信验证码 + /// + public static Task SignUpOrLogInByMobilePhoneAsync(string mobilePhoneNumber, string smsCode, CancellationToken cancellationToken) + { + Dictionary strs = new Dictionary() + { + { "mobilePhoneNumber", mobilePhoneNumber }, + { "smsCode", smsCode } + }; + return UserController.LogInWithParametersAsync("usersByMobilePhone", strs, cancellationToken).OnSuccess(t => + { + var user = (AVUser)AVObject.CreateWithoutData(null); + user.HandleFetchResult(t.Result); + return SaveCurrentUserAsync(user).OnSuccess(_ => user); + }).Unwrap(); + } + + /// + /// 手机号一键登录 + /// + /// signup or login by mobile phone async. + /// 手机号 + /// 短信验证码 + public static Task SignUpOrLogInByMobilePhoneAsync(string mobilePhoneNumber, string smsCode) + { + return AVUser.SignUpOrLogInByMobilePhoneAsync(mobilePhoneNumber, smsCode, CancellationToken.None); + } + + #region mobile sms shortcode sign up & log in. + /// + /// Send sign up sms code async. + /// + /// The sign up sms code async. + /// Mobile phone number. + public static Task SendSignUpSmsCodeAsync(string mobilePhoneNumber) + { + return AVCloud.RequestSMSCodeAsync(mobilePhoneNumber); + } + + /// + /// Sign up by mobile phone async. + /// + /// The up by mobile phone async. + /// Mobile phone number. + /// Sms code. + public static Task SignUpByMobilePhoneAsync(string mobilePhoneNumber, string smsCode) + { + return AVUser.SignUpOrLogInByMobilePhoneAsync(mobilePhoneNumber, smsCode); + } + + /// + /// Send log in sms code async. + /// + /// The log in sms code async. + /// Mobile phone number. + public static Task SendLogInSmsCodeAsync(string mobilePhoneNumber) + { + return AVUser.RequestLogInSmsCodeAsync(mobilePhoneNumber); + } + + /// + /// Log in by mobile phone async. + /// + /// The in by mobile phone async. + /// Mobile phone number. + /// Sms code. + public static Task LogInByMobilePhoneAsync(string mobilePhoneNumber, string smsCode) + { + return AVUser.LogInBySmsCodeAsync(mobilePhoneNumber, smsCode); + } + #endregion + #endregion + + #region 重置密码 + /// + /// 请求重置密码,需要传入注册时使用的手机号。 + /// + /// 注册时使用的手机号 + /// + public static Task RequestPasswordResetBySmsCode(string mobilePhoneNumber) + { + return AVUser.RequestPasswordResetBySmsCode(mobilePhoneNumber, null, CancellationToken.None); + } + + /// + /// 请求重置密码,需要传入注册时使用的手机号。 + /// + /// 注册时使用的手机号 + /// cancellationToken + /// + public static Task RequestPasswordResetBySmsCode(string mobilePhoneNumber, CancellationToken cancellationToken) + { + return RequestPasswordResetBySmsCode(mobilePhoneNumber, null, cancellationToken); + } + + /// + /// 请求重置密码,需要传入注册时使用的手机号。 + /// + /// 注册时使用的手机号 + /// Validate token. + /// + public static Task RequestPasswordResetBySmsCode(string mobilePhoneNumber, string validateToken) + { + return AVUser.RequestPasswordResetBySmsCode(mobilePhoneNumber, validateToken, CancellationToken.None); + } + + /// + /// 请求重置密码,需要传入注册时使用的手机号。 + /// + /// 注册时使用的手机号 + /// Validate token. + /// cancellationToken + /// + public static Task RequestPasswordResetBySmsCode(string mobilePhoneNumber, string validateToken, CancellationToken cancellationToken) + { + string currentSessionToken = AVUser.CurrentSessionToken; + Dictionary strs = new Dictionary() + { + { "mobilePhoneNumber", mobilePhoneNumber }, + }; + if (String.IsNullOrEmpty(validateToken)) + { + strs.Add("validate_token", validateToken); + } + var command = new AVCommand("requestPasswordResetBySmsCode", + method: "POST", + sessionToken: currentSessionToken, + data: strs); + return AVPlugins.Instance.CommandRunner.RunCommandAsync(command).ContinueWith(t => + { + return AVClient.IsSuccessStatusCode(t.Result.Item1); + }); + } + + /// + /// 通过验证码重置密码。 + /// + /// 新密码 + /// 6位数验证码 + /// + public static Task ResetPasswordBySmsCodeAsync(string newPassword, string smsCode) + { + return AVUser.ResetPasswordBySmsCodeAsync(newPassword, smsCode, CancellationToken.None); + } + + /// + /// 通过验证码重置密码。 + /// + /// 新密码 + /// 6位数验证码 + /// cancellationToken + /// + public static Task ResetPasswordBySmsCodeAsync(string newPassword, string smsCode, CancellationToken cancellationToken) + { + string currentSessionToken = AVUser.CurrentSessionToken; + Dictionary strs = new Dictionary() + { + { "password", newPassword } + }; + var command = new AVCommand("resetPasswordBySmsCode/" + smsCode, + method: "PUT", + sessionToken: currentSessionToken, + data: strs); + return AVPlugins.Instance.CommandRunner.RunCommandAsync(command).ContinueWith(t => + { + return AVClient.IsSuccessStatusCode(t.Result.Item1); + }); + } + + /// + /// 发送认证码到需要认证的手机上 + /// + /// 手机号 + /// + public static Task RequestMobilePhoneVerifyAsync(string mobilePhoneNumber) + { + return AVUser.RequestMobilePhoneVerifyAsync(mobilePhoneNumber, null, CancellationToken.None); + } + + /// + /// 发送认证码到需要认证的手机上 + /// + /// 手机号 + /// Validate token. + /// + public static Task RequestMobilePhoneVerifyAsync(string mobilePhoneNumber, string validateToken) + { + return AVUser.RequestMobilePhoneVerifyAsync(mobilePhoneNumber, validateToken, CancellationToken.None); + } + + /// + /// 发送认证码到需要认证的手机上 + /// + /// 手机号 + /// CancellationToken + /// + public static Task RequestMobilePhoneVerifyAsync(string mobilePhoneNumber, CancellationToken cancellationToken) + { + return RequestMobilePhoneVerifyAsync(mobilePhoneNumber, null, cancellationToken); + } + + /// + /// 发送认证码到需要认证的手机上 + /// + /// 手机号 + /// Validate token. + /// CancellationToken + /// + public static Task RequestMobilePhoneVerifyAsync(string mobilePhoneNumber, string validateToken, CancellationToken cancellationToken) + { + string currentSessionToken = AVUser.CurrentSessionToken; + Dictionary strs = new Dictionary() + { + { "mobilePhoneNumber", mobilePhoneNumber } + }; + if (String.IsNullOrEmpty(validateToken)) + { + strs.Add("validate_token", validateToken); + } + var command = new AVCommand("requestMobilePhoneVerify", + method: "POST", + sessionToken: currentSessionToken, + data: strs); + return AVPlugins.Instance.CommandRunner.RunCommandAsync(command).ContinueWith(t => + { + return AVClient.IsSuccessStatusCode(t.Result.Item1); + }); + } + + /// + /// 验证手机验证码是否为有效值 + /// + /// 手机收到的验证码 + /// 手机号 + /// + public static Task VerifyMobilePhoneAsync(string code, string mobilePhoneNumber) + { + return AVUser.VerifyMobilePhoneAsync(code, mobilePhoneNumber, CancellationToken.None); + } + + /// + /// 验证手机验证码是否为有效值 + /// + /// 手机收到的验证码 + /// 手机号,可选 + /// + /// + public static Task VerifyMobilePhoneAsync(string code, string mobilePhoneNumber, CancellationToken cancellationToken) + { + var command = new AVCommand("verifyMobilePhone/" + code.Trim() + "?mobilePhoneNumber=" + mobilePhoneNumber.Trim(), + method: "POST", + sessionToken: null, + data: null); + return AVPlugins.Instance.CommandRunner.RunCommandAsync(command).ContinueWith(t => + { + return AVClient.IsSuccessStatusCode(t.Result.Item1); + }); + } + + /// + /// 验证手机验证码是否为有效值 + /// + /// 手机收到的验证码 + /// + public static Task VerifyMobilePhoneAsync(string code) + { + var command = new AVCommand("verifyMobilePhone/" + code.Trim(), + method: "POST", + sessionToken: null, + data: null); + + return AVPlugins.Instance.CommandRunner.RunCommandAsync(command).ContinueWith(t => + { + return AVClient.IsSuccessStatusCode(t.Result.Item1); + }); + } + + /// + /// 验证手机验证码是否为有效值 + /// + /// 手机收到的验证码 + /// cancellationToken + /// + public static Task VerifyMobilePhoneAsync(string code, CancellationToken cancellationToken) + { + return AVUser.VerifyMobilePhoneAsync(code, CancellationToken.None); + } + + #endregion + + #region 邮箱验证 + /// + /// 申请发送验证邮箱的邮件,一周之内有效 + /// 如果该邮箱已经验证通过,会直接返回 True,并不会真正发送邮件 + /// 注意,不能频繁的调用此接口,一天之内只允许向同一个邮箱发送验证邮件 3 次,超过调用次数,会直接返回错误 + /// + /// 邮箱地址 + /// + public static Task RequestEmailVerifyAsync(string email) + { + Dictionary strs = new Dictionary() + { + { "email", email } + }; + var command = new AVCommand("requestEmailVerify", + method: "POST", + sessionToken: null, + data: strs); + return AVPlugins.Instance.CommandRunner.RunCommandAsync(command).ContinueWith(t => + { + return AVClient.IsSuccessStatusCode(t.Result.Item1); + }); + } + #endregion + + #region in no-local-storage enviroment + + internal Task Create() + { + return this.Create(CancellationToken.None); + } + internal Task Create(CancellationToken cancellationToken) + { + return taskQueue.Enqueue(toAwait => Create(toAwait, cancellationToken), + cancellationToken); + } + + internal Task Create(Task toAwait, CancellationToken cancellationToken) + { + if (AuthData == null) + { + // TODO (hallucinogen): make an Extension of Task to create Task with exception/canceled. + if (string.IsNullOrEmpty(Username)) + { + TaskCompletionSource tcs = new TaskCompletionSource(); + tcs.TrySetException(new InvalidOperationException("Cannot sign up user with an empty name.")); + return tcs.Task; + } + if (string.IsNullOrEmpty(Password)) + { + TaskCompletionSource tcs = new TaskCompletionSource(); + tcs.TrySetException(new InvalidOperationException("Cannot sign up user with an empty password.")); + return tcs.Task; + } + } + if (!string.IsNullOrEmpty(ObjectId)) + { + TaskCompletionSource tcs = new TaskCompletionSource(); + tcs.TrySetException(new InvalidOperationException("Cannot sign up a user that already exists.")); + return tcs.Task; + } + + IDictionary currentOperations = StartSave(); + + return toAwait.OnSuccess(_ => + { + return UserController.SignUpAsync(State, currentOperations, cancellationToken); + }).Unwrap().ContinueWith(t => + { + if (t.IsFaulted || t.IsCanceled) + { + HandleFailedSave(currentOperations); + } + else + { + var serverState = t.Result; + HandleSave(serverState); + } + return t; + }).Unwrap(); + } + #endregion + + + #region task session token for http request + internal static Task TakeSessionToken(string sesstionToken = null) + { + var sessionTokenTask = Task.FromResult(sesstionToken); + if (sesstionToken == null) + sessionTokenTask = AVUser.GetCurrentAsync().OnSuccess(u => + { + if (u.Result != null) + return u.Result.SessionToken; + return null; + }); + return sessionTokenTask; + } + #endregion + + + #region AVUser Extension + public IDictionary> GetAuthData() { + return AuthData; + } + + /// + /// use 3rd auth data to sign up or log in.if user with the same auth data exits,it will transfer as log in. + /// + /// OAuth data, like {"accessToken":"xxxxxx"} + /// auth platform,maybe "facebook"/"twiiter"/"weibo"/"weixin" .etc + /// + /// + public static Task LogInWithAuthDataAsync(IDictionary data, + string platform, + AVUserAuthDataLogInOption options = null, + CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { + if (options == null) { + options = new AVUserAuthDataLogInOption(); + } + return AVUser.LogInWithAsync(platform, data, options.FailOnNotExist, cancellationToken); + } + + public static Task LogInWithAuthDataAndUnionIdAsync( + IDictionary authData, + string platform, + string unionId, + AVUserAuthDataLogInOption options = null, + CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { + if (options == null) { + options = new AVUserAuthDataLogInOption(); + } + MergeAuthData(authData, unionId, options); + return AVUser.LogInWithAsync(platform, authData, options.FailOnNotExist, cancellationToken); + } + + public static Task LogInAnonymouslyAsync(CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { + var data = new Dictionary { + { "id", Guid.NewGuid().ToString() } + }; + var options = new AVUserAuthDataLogInOption(); + return LogInWithAuthDataAsync(data, "anonymous", options, cancellationToken); + } + + [Obsolete("please use LogInWithAuthDataAsync instead.")] + public static Task LogInWithAsync(string authType, IDictionary data, CancellationToken cancellationToken) { + return AVUser.LogInWithAsync(authType, data, false, cancellationToken); + } + + /// + /// link a 3rd auth account to the user. + /// + /// AVUser instance + /// OAuth data, like {"accessToken":"xxxxxx"} + /// auth platform,maybe "facebook"/"twiiter"/"weibo"/"weixin" .etc + /// + /// + public Task AssociateAuthDataAsync(IDictionary data, string platform, CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { + return LinkWithAsync(platform, data, cancellationToken); + } + + public Task AssociateAuthDataAndUnionIdAsync( + IDictionary authData, + string platform, + string unionId, + AVUserAuthDataLogInOption options = null, + CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { + if (options == null) { + options = new AVUserAuthDataLogInOption(); + } + MergeAuthData(authData, unionId, options); + return LinkWithAsync(platform, authData, cancellationToken); + } + + /// + /// unlink a 3rd auth account from the user. + /// + /// AVUser instance + /// auth platform,maybe "facebook"/"twiiter"/"weibo"/"weixin" .etc + /// + /// + public Task DisassociateWithAuthDataAsync(string platform, CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { + return UnlinkFromAsync(platform, cancellationToken); + } + + /// 合并为支持 AuthData 的格式 + static void MergeAuthData(IDictionary authData, string unionId, AVUserAuthDataLogInOption options) { + authData["platform"] = options.UnionIdPlatform; + authData["main_account"] = options.AsMainAccount; + authData["unionid"] = unionId; + } + #endregion + } +} diff --git a/Storage/Source/Public/AVUserAuthDataLogInOption.cs b/Storage/Source/Public/AVUserAuthDataLogInOption.cs new file mode 100644 index 0000000..1918c2b --- /dev/null +++ b/Storage/Source/Public/AVUserAuthDataLogInOption.cs @@ -0,0 +1,33 @@ +using System; + +namespace LeanCloud { + /// + /// AuthData 登陆选项 + /// + public class AVUserAuthDataLogInOption { + + /// + /// unionId platform + /// + /// unionId platform. + public string UnionIdPlatform; + + /// + /// If true, the unionId will be associated with the user. + /// + /// true If true, the unionId will be associated with the user. false. + public bool AsMainAccount; + + /// + /// If true, the login request will fail when no user matches this authData exists. + /// + /// true If true, the login request will fail when no user matches this authData exists. false. + public bool FailOnNotExist; + + public AVUserAuthDataLogInOption() { + UnionIdPlatform = "weixin"; + AsMainAccount = false; + FailOnNotExist = false; + } + } +} diff --git a/Storage/Source/Public/IAVQuery.cs b/Storage/Source/Public/IAVQuery.cs new file mode 100644 index 0000000..a4ee243 --- /dev/null +++ b/Storage/Source/Public/IAVQuery.cs @@ -0,0 +1,2068 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using LeanCloud.Storage.Internal; + +namespace LeanCloud +{ + /// + /// Query 对象的基础接口 + /// + public interface IAVQuery + { + + } + + /// + /// LeanCloud 存储对象的接触接口 + /// + public interface IAVObject + { + + } + + public abstract class AVQueryBase : IAVQuery + where T : IAVObject + { + internal string className; + internal Dictionary where; + internal ReadOnlyCollection orderBy; + internal ReadOnlyCollection includes; + internal ReadOnlyCollection selectedKeys; + internal String redirectClassNameForKey; + internal int? skip; + internal int? limit; + + /// + /// 构建查询字符串 + /// + /// 是否包含 ClassName + /// + public IDictionary BuildParameters(bool includeClassName = false) + { + Dictionary result = new Dictionary(); + if (where != null) + { + result["where"] = PointerOrLocalIdEncoder.Instance.Encode(where); + } + if (orderBy != null) + { + result["order"] = string.Join(",", orderBy.ToArray()); + } + if (skip != null) + { + result["skip"] = skip.Value; + } + if (limit != null) + { + result["limit"] = limit.Value; + } + if (includes != null) + { + result["include"] = string.Join(",", includes.ToArray()); + } + if (selectedKeys != null) + { + result["keys"] = string.Join(",", selectedKeys.ToArray()); + } + if (includeClassName) + { + result["className"] = className; + } + if (redirectClassNameForKey != null) + { + result["redirectClassNameForKey"] = redirectClassNameForKey; + } + return result; + } + + public virtual Dictionary Where + { + get + { + return this.where; + } + set + { + this.where = value; + } + } + + public virtual IDictionary MergeWhere(IDictionary primary, IDictionary secondary) + { + if (secondary == null) + { + return primary; + } + var newWhere = new Dictionary(primary); + foreach (var pair in secondary) + { + var condition = pair.Value as IDictionary; + if (newWhere.ContainsKey(pair.Key)) + { + var oldCondition = newWhere[pair.Key] as IDictionary; + if (oldCondition == null || condition == null) + { + throw new ArgumentException("More than one where clause for the given key provided."); + } + var newCondition = new Dictionary(oldCondition); + foreach (var conditionPair in condition) + { + if (newCondition.ContainsKey(conditionPair.Key)) + { + throw new ArgumentException("More than one condition for the given key provided."); + } + newCondition[conditionPair.Key] = conditionPair.Value; + } + newWhere[pair.Key] = newCondition; + } + else + { + newWhere[pair.Key] = pair.Value; + } + } + return newWhere; + } + } + + public abstract class AVQueryPair + where S : IAVQuery + where T : IAVObject + + { + protected readonly string className; + protected readonly Dictionary where; + protected readonly ReadOnlyCollection orderBy; + protected readonly ReadOnlyCollection includes; + protected readonly ReadOnlyCollection selectedKeys; + protected readonly String redirectClassNameForKey; + protected readonly int? skip; + protected readonly int? limit; + + internal string ClassName { get { return className; } } + + private string relativeUri; + internal string RelativeUri + { + get + { + string rtn = string.Empty; + if (string.IsNullOrEmpty(relativeUri)) + { + rtn = "classes/" + Uri.EscapeDataString(this.className); + } + else + { + rtn = relativeUri; + } + return rtn; + } + set + { + relativeUri = value; + } + } + public Dictionary Condition + { + get { return this.where; } + } + + protected AVQueryPair() + { + + } + + public abstract S CreateInstance(IDictionary where = null, + IEnumerable replacementOrderBy = null, + IEnumerable thenBy = null, + int? skip = null, + int? limit = null, + IEnumerable includes = null, + IEnumerable selectedKeys = null, + String redirectClassNameForKey = null); + + /// + /// Private constructor for composition of queries. A Source query is required, + /// but the remaining values can be null if they won't be changed in this + /// composition. + /// + protected AVQueryPair(AVQueryPair source, + IDictionary where = null, + IEnumerable replacementOrderBy = null, + IEnumerable thenBy = null, + int? skip = null, + int? limit = null, + IEnumerable includes = null, + IEnumerable selectedKeys = null, + String redirectClassNameForKey = null) + { + if (source == null) + { + throw new ArgumentNullException("Source"); + } + + className = source.className; + this.where = source.where; + this.orderBy = source.orderBy; + this.skip = source.skip; + this.limit = source.limit; + this.includes = source.includes; + this.selectedKeys = source.selectedKeys; + this.redirectClassNameForKey = source.redirectClassNameForKey; + + if (where != null) + { + var newWhere = MergeWhereClauses(where); + this.where = new Dictionary(newWhere); + } + + if (replacementOrderBy != null) + { + this.orderBy = new ReadOnlyCollection(replacementOrderBy.ToList()); + } + + if (thenBy != null) + { + if (this.orderBy == null) + { + throw new ArgumentException("You must call OrderBy before calling ThenBy."); + } + var newOrderBy = new List(this.orderBy); + newOrderBy.AddRange(thenBy); + this.orderBy = new ReadOnlyCollection(newOrderBy); + } + + // Remove duplicates. + if (this.orderBy != null) + { + var newOrderBy = new HashSet(this.orderBy); + this.orderBy = new ReadOnlyCollection(newOrderBy.ToList()); + } + + if (skip != null) + { + this.skip = (this.skip ?? 0) + skip; + } + + if (limit != null) + { + this.limit = limit; + } + + if (includes != null) + { + var newIncludes = MergeIncludes(includes); + this.includes = new ReadOnlyCollection(newIncludes.ToList()); + } + + if (selectedKeys != null) + { + var newSelectedKeys = MergeSelectedKeys(selectedKeys); + this.selectedKeys = new ReadOnlyCollection(newSelectedKeys.ToList()); + } + + if (redirectClassNameForKey != null) + { + this.redirectClassNameForKey = redirectClassNameForKey; + } + } + + public AVQueryPair(string className) + { + if (string.IsNullOrEmpty(className)) + { + throw new ArgumentNullException("className", "Must specify a AVObject class name when creating a AVQuery."); + } + this.className = className; + } + + private HashSet MergeIncludes(IEnumerable includes) + { + if (this.includes == null) + { + return new HashSet(includes); + } + var newIncludes = new HashSet(this.includes); + foreach (var item in includes) + { + newIncludes.Add(item); + } + return newIncludes; + } + + private HashSet MergeSelectedKeys(IEnumerable selectedKeys) + { + if (this.selectedKeys == null) + { + return new HashSet(selectedKeys); + } + var newSelectedKeys = new HashSet(this.selectedKeys); + foreach (var item in selectedKeys) + { + newSelectedKeys.Add(item); + } + return newSelectedKeys; + } + + private IDictionary MergeWhereClauses(IDictionary where) + { + return MergeWhere(this.where, where); + } + + public virtual IDictionary MergeWhere(IDictionary primary, IDictionary secondary) + { + if (secondary == null) + { + return primary; + } + if (primary == null) + { + return secondary; + } + var newWhere = new Dictionary(primary); + foreach (var pair in secondary) + { + var condition = pair.Value as IDictionary; + if (newWhere.ContainsKey(pair.Key)) + { + var oldCondition = newWhere[pair.Key] as IDictionary; + if (oldCondition == null || condition == null) + { + throw new ArgumentException("More than one where clause for the given key provided."); + } + var newCondition = new Dictionary(oldCondition); + foreach (var conditionPair in condition) + { + if (newCondition.ContainsKey(conditionPair.Key)) + { + throw new ArgumentException("More than one condition for the given key provided."); + } + newCondition[conditionPair.Key] = conditionPair.Value; + } + newWhere[pair.Key] = newCondition; + } + else + { + newWhere[pair.Key] = pair.Value; + } + } + return newWhere; + } + + /// + /// Constructs a query that is the or of the given queries. + /// + /// The list of AVQueries to 'or' together. + /// A AVeQquery that is the 'or' of the passed in queries. + public static Q Or(IEnumerable queries) + where Q : AVQueryBase + where O : IAVObject + { + string className = null; + var orValue = new List>(); + // We need to cast it to non-generic IEnumerable because of AOT-limitation + var nonGenericQueries = (IEnumerable)queries; + Q current = null; + foreach (var obj in nonGenericQueries) + { + var q = (Q)obj; + current = q; + if (className != null && q.className != className) + { + throw new ArgumentException( + "All of the queries in an or query must be on the same class."); + } + className = q.className; + var parameters = q.BuildParameters(); + if (parameters.Count == 0) + { + continue; + } + object where; + if (!parameters.TryGetValue("where", out where) || parameters.Count > 1) + { + throw new ArgumentException( + "None of the queries in an or query can have non-filtering clauses"); + } + orValue.Add(where as IDictionary); + } + current.Where = new Dictionary() + { + {"$or", orValue} + }; + return current; + } + + #region Order By + + /// + /// Sorts the results in ascending order by the given key. + /// This will override any existing ordering for the query. + /// + /// The key to order by. + /// A new query with the additional constraint. + public virtual S OrderBy(string key) + { + return CreateInstance(replacementOrderBy: new List { key }); + } + + /// + /// Sorts the results in descending order by the given key. + /// This will override any existing ordering for the query. + /// + /// The key to order by. + /// A new query with the additional constraint. + public virtual S OrderByDescending(string key) + { + return CreateInstance(replacementOrderBy: new List { "-" + key }); + } + + /// + /// Sorts the results in ascending order by the given key, after previous + /// ordering has been applied. + /// + /// This method can only be called if there is already an + /// or + /// on this query. + /// + /// The key to order by. + /// A new query with the additional constraint. + public virtual S ThenBy(string key) + { + return CreateInstance(thenBy: new List { key }); + } + + /// + /// Sorts the results in descending order by the given key, after previous + /// ordering has been applied. + /// + /// This method can only be called if there is already an + /// or on this query. + /// + /// The key to order by. + /// A new query with the additional constraint. + public virtual S ThenByDescending(string key) + { + return CreateInstance(thenBy: new List { "-" + key }); + } + + #endregion + + /// + /// Include nested AVObjects for the provided key. You can use dot notation + /// to specify which fields in the included objects should also be fetched. + /// + /// The key that should be included. + /// A new query with the additional constraint. + public virtual S Include(string key) + { + return CreateInstance(includes: new List { key }); + } + + /// + /// Restrict the fields of returned AVObjects to only include the provided key. + /// If this is called multiple times, then all of the keys specified in each of + /// the calls will be included. + /// + /// The key that should be included. + /// A new query with the additional constraint. + public virtual S Select(string key) + { + return CreateInstance(selectedKeys: new List { key }); + } + + /// + /// Skips a number of results before returning. This is useful for pagination + /// of large queries. Chaining multiple skips together will cause more results + /// to be skipped. + /// + /// The number of results to skip. + /// A new query with the additional constraint. + public virtual S Skip(int count) + { + return CreateInstance(skip: count); + } + + /// + /// Controls the maximum number of results that are returned. Setting a negative + /// limit denotes retrieval without a limit. Chaining multiple limits + /// results in the last limit specified being used. The default limit is + /// 100, with a maximum of 1000 results being returned at a time. + /// + /// The maximum number of results to return. + /// A new query with the additional constraint. + public virtual S Limit(int count) + { + return CreateInstance(limit: count); + } + + internal virtual S RedirectClassName(String key) + { + return CreateInstance(redirectClassNameForKey: key); + } + + #region Where + + /// + /// Adds a constraint to the query that requires a particular key's value to be + /// contained in the provided list of values. + /// + /// The key to check. + /// The values that will match. + /// A new query with the additional constraint. + public virtual S WhereContainedIn(string key, IEnumerable values) + { + return CreateInstance(where: new Dictionary { + { key, new Dictionary{{"$in", values.ToList()}}} + }); + } + + /// + /// Add a constraint to the querey that requires a particular key's value to be + /// a list containing all of the elements in the provided list of values. + /// + /// The key to check. + /// The values that will match. + /// A new query with the additional constraint. + public virtual S WhereContainsAll(string key, IEnumerable values) + { + return CreateInstance(where: new Dictionary { + { key, new Dictionary{{"$all", values.ToList()}}} + }); + } + + /// + /// Adds a constraint for finding string values that contain a provided string. + /// This will be slow for large data sets. + /// + /// The key that the string to match is stored in. + /// The substring that the value must contain. + /// A new query with the additional constraint. + public virtual S WhereContains(string key, string substring) + { + return CreateInstance(where: new Dictionary { + { key, new Dictionary{{"$regex", RegexQuote(substring)}}} + }); + } + + /// + /// Adds a constraint for finding objects that do not contain a given key. + /// + /// The key that should not exist. + /// A new query with the additional constraint. + public virtual S WhereDoesNotExist(string key) + { + return CreateInstance(where: new Dictionary{ + { key, new Dictionary{{"$exists", false}}} + }); + } + + /// + /// Adds a constraint to the query that requires that a particular key's value + /// does not match another AVQuery. This only works on keys whose values are + /// AVObjects or lists of AVObjects. + /// + /// The key to check. + /// The query that the value should not match. + /// A new query with the additional constraint. + public virtual S WhereDoesNotMatchQuery(string key, AVQuery query) + where TOther : AVObject + { + return CreateInstance(where: new Dictionary { + { key, new Dictionary{{"$notInQuery", query.BuildParameters(true)}}} + }); + } + + /// + /// Adds a constraint for finding string values that end with a provided string. + /// This will be slow for large data sets. + /// + /// The key that the string to match is stored in. + /// The substring that the value must end with. + /// A new query with the additional constraint. + public virtual S WhereEndsWith(string key, string suffix) + { + return CreateInstance(where: new Dictionary { + { key, new Dictionary{{"$regex", RegexQuote(suffix) + "$"}}} + }); + } + + /// + /// Adds a constraint to the query that requires a particular key's value to be + /// equal to the provided value. + /// + /// The key to check. + /// The value that the AVObject must contain. + /// A new query with the additional constraint. + public virtual S WhereEqualTo(string key, object value) + { + return CreateInstance(where: new Dictionary { + { key, value} + }); + } + + /// + /// Adds a constraint to the query that requires a particular key's size to be + /// equal to the provided size. + /// + /// The size equal to. + /// The key to check. + /// The value that the size must be. + /// A new query with the additional constraint. + public virtual S WhereSizeEqualTo(string key, uint size) + { + return CreateInstance(where: new Dictionary { + { key, new Dictionary{{"$size", size}}} + }); + } + + /// + /// Adds a constraint for finding objects that contain a given key. + /// + /// The key that should exist. + /// A new query with the additional constraint. + public virtual S WhereExists(string key) + { + return CreateInstance(where: new Dictionary{ + { key, new Dictionary{{"$exists", true}}} + }); + } + + /// + /// Adds a constraint to the query that requires a particular key's value to be + /// greater than the provided value. + /// + /// The key to check. + /// The value that provides a lower bound. + /// A new query with the additional constraint. + public virtual S WhereGreaterThan(string key, object value) + { + return CreateInstance(where: new Dictionary{ + { key, new Dictionary{{"$gt", value}}} + }); + } + + /// + /// Adds a constraint to the query that requires a particular key's value to be + /// greater or equal to than the provided value. + /// + /// The key to check. + /// The value that provides a lower bound. + /// A new query with the additional constraint. + public virtual S WhereGreaterThanOrEqualTo(string key, object value) + { + return CreateInstance(where: new Dictionary{ + { key, new Dictionary{{"$gte", value}}} + }); + } + + /// + /// Adds a constraint to the query that requires a particular key's value to be + /// less than the provided value. + /// + /// The key to check. + /// The value that provides an upper bound. + /// A new query with the additional constraint. + public virtual S WhereLessThan(string key, object value) + { + return CreateInstance(where: new Dictionary{ + { key, new Dictionary{{"$lt", value}}} + }); + } + + /// + /// Adds a constraint to the query that requires a particular key's value to be + /// less than or equal to the provided value. + /// + /// The key to check. + /// The value that provides a lower bound. + /// A new query with the additional constraint. + public virtual S WhereLessThanOrEqualTo(string key, object value) + { + return CreateInstance(where: new Dictionary{ + { key, new Dictionary{{"$lte", value}}} + }); + } + + /// + /// Adds a regular expression constraint for finding string values that match the provided + /// regular expression. This may be slow for large data sets. + /// + /// The key that the string to match is stored in. + /// The regular expression pattern to match. The Regex must + /// have the options flag set. + /// Any of the following supported PCRE modifiers: + /// i - Case insensitive search + /// m Search across multiple lines of input + /// A new query with the additional constraint. + public virtual S WhereMatches(string key, Regex regex, string modifiers) + { + if (!regex.Options.HasFlag(RegexOptions.ECMAScript)) + { + throw new ArgumentException( + "Only ECMAScript-compatible regexes are supported. Please use the ECMAScript RegexOptions flag when creating your regex."); + } + return CreateInstance(where: new Dictionary { + { key, EncodeRegex(regex, modifiers)} + }); + } + + /// + /// Adds a regular expression constraint for finding string values that match the provided + /// regular expression. This may be slow for large data sets. + /// + /// The key that the string to match is stored in. + /// The regular expression pattern to match. The Regex must + /// have the options flag set. + /// A new query with the additional constraint. + public virtual S WhereMatches(string key, Regex regex) + { + return WhereMatches(key, regex, null); + } + + /// + /// Adds a regular expression constraint for finding string values that match the provided + /// regular expression. This may be slow for large data sets. + /// + /// The key that the string to match is stored in. + /// The PCRE regular expression pattern to match. + /// Any of the following supported PCRE modifiers: + /// i - Case insensitive search + /// m Search across multiple lines of input + /// A new query with the additional constraint. + public virtual S WhereMatches(string key, string pattern, string modifiers = null) + { + return WhereMatches(key, new Regex(pattern, RegexOptions.ECMAScript), modifiers); + } + + /// + /// Adds a regular expression constraint for finding string values that match the provided + /// regular expression. This may be slow for large data sets. + /// + /// The key that the string to match is stored in. + /// The PCRE regular expression pattern to match. + /// A new query with the additional constraint. + public virtual S WhereMatches(string key, string pattern) + { + return WhereMatches(key, pattern, null); + } + + /// + /// Adds a constraint to the query that requires a particular key's value + /// to match a value for a key in the results of another AVQuery. + /// + /// The key whose value is being checked. + /// The key in the objects from the subquery to look in. + /// The subquery to run + /// A new query with the additional constraint. + public virtual S WhereMatchesKeyInQuery(string key, + string keyInQuery, + AVQuery query) where TOther : AVObject + { + var parameters = new Dictionary { + { "query", query.BuildParameters(true)}, + { "key", keyInQuery} + }; + return CreateInstance(where: new Dictionary { + { key, new Dictionary{{"$select", parameters}}} + }); + } + + /// + /// Adds a constraint to the query that requires a particular key's value + /// does not match any value for a key in the results of another AVQuery. + /// + /// The key whose value is being checked. + /// The key in the objects from the subquery to look in. + /// The subquery to run + /// A new query with the additional constraint. + public virtual S WhereDoesNotMatchesKeyInQuery(string key, + string keyInQuery, + AVQuery query) where TOther : AVObject + { + var parameters = new Dictionary { + { "query", query.BuildParameters(true)}, + { "key", keyInQuery} + }; + return CreateInstance(where: new Dictionary { + { key, new Dictionary{{"$dontSelect", parameters}}} + }); + } + + /// + /// Adds a constraint to the query that requires that a particular key's value + /// matches another AVQuery. This only works on keys whose values are + /// AVObjects or lists of AVObjects. + /// + /// The key to check. + /// The query that the value should match. + /// A new query with the additional constraint. + public virtual S WhereMatchesQuery(string key, AVQuery query) + where TOther : AVObject + { + return CreateInstance(where: new Dictionary { + { key, new Dictionary{{"$inQuery", query.BuildParameters(true)}}} + }); + } + + /// + /// Adds a proximity-based constraint for finding objects with keys whose GeoPoint + /// values are near the given point. + /// + /// The key that the AVGeoPoint is stored in. + /// The reference AVGeoPoint. + /// A new query with the additional constraint. + public virtual S WhereNear(string key, AVGeoPoint point) + { + return CreateInstance(where: new Dictionary { + { key, new Dictionary{{"$nearSphere", point}}} + }); + } + + /// + /// Adds a constraint to the query that requires a particular key's value to be + /// contained in the provided list of values. + /// + /// The key to check. + /// The values that will match. + /// A new query with the additional constraint. + public virtual S WhereNotContainedIn(string key, IEnumerable values) + { + return CreateInstance(where: new Dictionary { + { key, new Dictionary{{"$nin", values.ToList()}}} + }); + } + + /// + /// Adds a constraint to the query that requires a particular key's value not + /// to be equal to the provided value. + /// + /// The key to check. + /// The value that that must not be equalled. + /// A new query with the additional constraint. + public virtual S WhereNotEqualTo(string key, object value) + { + return CreateInstance(where: new Dictionary { + { key, new Dictionary{{"$ne", value}}} + }); + } + + /// + /// Adds a constraint for finding string values that start with the provided string. + /// This query will use the backend index, so it will be fast even with large data sets. + /// + /// The key that the string to match is stored in. + /// The substring that the value must start with. + /// A new query with the additional constraint. + public virtual S WhereStartsWith(string key, string suffix) + { + return CreateInstance(where: new Dictionary { + { key, new Dictionary{{"$regex", "^" + RegexQuote(suffix)}}} + }); + } + + /// + /// Add a constraint to the query that requires a particular key's coordinates to be + /// contained within a given rectangular geographic bounding box. + /// + /// The key to be constrained. + /// The lower-left inclusive corner of the box. + /// The upper-right inclusive corner of the box. + /// A new query with the additional constraint. + public virtual S WhereWithinGeoBox(string key, + AVGeoPoint southwest, + AVGeoPoint northeast) + { + + return this.CreateInstance(where: new Dictionary + { + { + key, + new Dictionary + { + { + "$within", + new Dictionary { + { "$box", new[] {southwest, northeast}} + } + } + } + } + }); + } + + /// + /// Adds a proximity-based constraint for finding objects with keys whose GeoPoint + /// values are near the given point and within the maximum distance given. + /// + /// The key that the AVGeoPoint is stored in. + /// The reference AVGeoPoint. + /// The maximum distance (in radians) of results to return. + /// A new query with the additional constraint. + public virtual S WhereWithinDistance( + string key, AVGeoPoint point, AVGeoDistance maxDistance) + { + var nearWhere = new Dictionary { + { key, new Dictionary{{"$nearSphere", point}}} + }; + var mergedWhere = MergeWhere(nearWhere, new Dictionary { + { key, new Dictionary{{"$maxDistance", maxDistance.Radians}}} + }); + return CreateInstance(where: mergedWhere); + } + + internal virtual S WhereRelatedTo(AVObject parent, string key) + { + return CreateInstance(where: new Dictionary { + { + "$relatedTo", + new Dictionary { + { "object", parent}, + { "key", key} + } + } + }); + } + + #endregion + + /// + /// Retrieves a list of AVObjects that satisfy this query from LeanCloud. + /// + /// The list of AVObjects that match this query. + public virtual Task> FindAsync() + { + return FindAsync(CancellationToken.None); + } + + /// + /// Retrieves a list of AVObjects that satisfy this query from LeanCloud. + /// + /// The cancellation token. + /// The list of AVObjects that match this query. + public abstract Task> FindAsync(CancellationToken cancellationToken); + + + /// + /// Retrieves at most one AVObject that satisfies this query. + /// + /// A single AVObject that satisfies this query, or else null. + public virtual Task FirstOrDefaultAsync() + { + return FirstOrDefaultAsync(CancellationToken.None); + } + + /// + /// Retrieves at most one AVObject that satisfies this query. + /// + /// The cancellation token. + /// A single AVObject that satisfies this query, or else null. + public abstract Task FirstOrDefaultAsync(CancellationToken cancellationToken); + + /// + /// Retrieves at most one AVObject that satisfies this query. + /// + /// A single AVObject that satisfies this query. + /// If no results match the query. + public virtual Task FirstAsync() + { + return FirstAsync(CancellationToken.None); + } + + /// + /// Retrieves at most one AVObject that satisfies this query. + /// + /// The cancellation token. + /// A single AVObject that satisfies this query. + /// If no results match the query. + public abstract Task FirstAsync(CancellationToken cancellationToken); + + /// + /// Counts the number of objects that match this query. + /// + /// The number of objects that match this query. + public virtual Task CountAsync() + { + return CountAsync(CancellationToken.None); + } + + /// + /// Counts the number of objects that match this query. + /// + /// The cancellation token. + /// The number of objects that match this query. + public abstract Task CountAsync(CancellationToken cancellationToken); + + /// + /// Constructs a AVObject whose id is already known by fetching data + /// from the server. + /// + /// ObjectId of the AVObject to fetch. + /// The AVObject for the given objectId. + public virtual Task GetAsync(string objectId) + { + return GetAsync(objectId, CancellationToken.None); + } + + /// + /// Constructs a AVObject whose id is already known by fetching data + /// from the server. + /// + /// ObjectId of the AVObject to fetch. + /// The cancellation token. + /// The AVObject for the given objectId. + public abstract Task GetAsync(string objectId, CancellationToken cancellationToken); + + internal object GetConstraint(string key) + { + return where == null ? null : where.GetOrDefault(key, null); + } + + /// + /// 构建查询字符串 + /// + /// 是否包含 ClassName + /// + public IDictionary BuildParameters(bool includeClassName = false) + { + Dictionary result = new Dictionary(); + if (where != null) + { + result["where"] = PointerOrLocalIdEncoder.Instance.Encode(where); + } + if (orderBy != null) + { + result["order"] = string.Join(",", orderBy.ToArray()); + } + if (skip != null) + { + result["skip"] = skip.Value; + } + if (limit != null) + { + result["limit"] = limit.Value; + } + if (includes != null) + { + result["include"] = string.Join(",", includes.ToArray()); + } + if (selectedKeys != null) + { + result["keys"] = string.Join(",", selectedKeys.ToArray()); + } + if (includeClassName) + { + result["className"] = className; + } + if (redirectClassNameForKey != null) + { + result["redirectClassNameForKey"] = redirectClassNameForKey; + } + return result; + } + + private string RegexQuote(string input) + { + return "\\Q" + input.Replace("\\E", "\\E\\\\E\\Q") + "\\E"; + } + + private string GetRegexOptions(Regex regex, string modifiers) + { + string result = modifiers ?? ""; + if (regex.Options.HasFlag(RegexOptions.IgnoreCase) && !modifiers.Contains("i")) + { + result += "i"; + } + if (regex.Options.HasFlag(RegexOptions.Multiline) && !modifiers.Contains("m")) + { + result += "m"; + } + return result; + } + + private IDictionary EncodeRegex(Regex regex, string modifiers) + { + var options = GetRegexOptions(regex, modifiers); + var dict = new Dictionary(); + dict["$regex"] = regex.ToString(); + if (!string.IsNullOrEmpty(options)) + { + dict["$options"] = options; + } + return dict; + } + } + + //public abstract class AVQueryBase : IAVQueryTuple + // where T : IAVObject + //{ + // protected readonly string className; + // protected readonly Dictionary where; + // protected readonly ReadOnlyCollection orderBy; + // protected readonly ReadOnlyCollection includes; + // protected readonly ReadOnlyCollection selectedKeys; + // protected readonly String redirectClassNameForKey; + // protected readonly int? skip; + // protected readonly int? limit; + + // internal string ClassName { get { return className; } } + + // private string relativeUri; + // internal string RelativeUri + // { + // get + // { + // string rtn = string.Empty; + // if (string.IsNullOrEmpty(relativeUri)) + // { + // rtn = "classes/" + Uri.EscapeDataString(this.className); + // } + // else + // { + // rtn = relativeUri; + // } + // return rtn; + // } + // set + // { + // relativeUri = value; + // } + // } + // public Dictionary Condition + // { + // get { return this.where; } + // } + + // protected AVQueryBase() + // { + + // } + + // internal abstract S CreateInstance(AVQueryBase source, + // IDictionary where = null, + // IEnumerable replacementOrderBy = null, + // IEnumerable thenBy = null, + // int? skip = null, + // int? limit = null, + // IEnumerable includes = null, + // IEnumerable selectedKeys = null, + // String redirectClassNameForKey = null); + + // /// + // /// Private constructor for composition of queries. A Source query is required, + // /// but the remaining values can be null if they won't be changed in this + // /// composition. + // /// + // protected AVQueryBase(AVQueryBase source, + // IDictionary where = null, + // IEnumerable replacementOrderBy = null, + // IEnumerable thenBy = null, + // int? skip = null, + // int? limit = null, + // IEnumerable includes = null, + // IEnumerable selectedKeys = null, + // String redirectClassNameForKey = null) + // { + // if (source == null) + // { + // throw new ArgumentNullException("Source"); + // } + + // className = source.className; + // this.where = source.where; + // this.orderBy = source.orderBy; + // this.skip = source.skip; + // this.limit = source.limit; + // this.includes = source.includes; + // this.selectedKeys = source.selectedKeys; + // this.redirectClassNameForKey = source.redirectClassNameForKey; + + // if (where != null) + // { + // var newWhere = MergeWhereClauses(where); + // this.where = new Dictionary(newWhere); + // } + + // if (replacementOrderBy != null) + // { + // this.orderBy = new ReadOnlyCollection(replacementOrderBy.ToList()); + // } + + // if (thenBy != null) + // { + // if (this.orderBy == null) + // { + // throw new ArgumentException("You must call OrderBy before calling ThenBy."); + // } + // var newOrderBy = new List(this.orderBy); + // newOrderBy.AddRange(thenBy); + // this.orderBy = new ReadOnlyCollection(newOrderBy); + // } + + // // Remove duplicates. + // if (this.orderBy != null) + // { + // var newOrderBy = new HashSet(this.orderBy); + // this.orderBy = new ReadOnlyCollection(newOrderBy.ToList()); + // } + + // if (skip != null) + // { + // this.skip = (this.skip ?? 0) + skip; + // } + + // if (limit != null) + // { + // this.limit = limit; + // } + + // if (includes != null) + // { + // var newIncludes = MergeIncludes(includes); + // this.includes = new ReadOnlyCollection(newIncludes.ToList()); + // } + + // if (selectedKeys != null) + // { + // var newSelectedKeys = MergeSelectedKeys(selectedKeys); + // this.selectedKeys = new ReadOnlyCollection(newSelectedKeys.ToList()); + // } + + // if (redirectClassNameForKey != null) + // { + // this.redirectClassNameForKey = redirectClassNameForKey; + // } + // } + + // public AVQueryBase(string className) + // { + // if (string.IsNullOrEmpty(className)) + // { + // throw new ArgumentNullException("className", "Must specify a AVObject class name when creating a AVQuery."); + // } + // this.className = className; + // } + + // private HashSet MergeIncludes(IEnumerable includes) + // { + // if (this.includes == null) + // { + // return new HashSet(includes); + // } + // var newIncludes = new HashSet(this.includes); + // foreach (var item in includes) + // { + // newIncludes.Add(item); + // } + // return newIncludes; + // } + + // private HashSet MergeSelectedKeys(IEnumerable selectedKeys) + // { + // if (this.selectedKeys == null) + // { + // return new HashSet(selectedKeys); + // } + // var newSelectedKeys = new HashSet(this.selectedKeys); + // foreach (var item in selectedKeys) + // { + // newSelectedKeys.Add(item); + // } + // return newSelectedKeys; + // } + + // private IDictionary MergeWhereClauses(IDictionary where) + // { + // return MergeWhere(this.where, where); + // } + + // public virtual IDictionary MergeWhere(IDictionary primary, IDictionary secondary) + // { + // if (secondary == null) + // { + // return primary; + // } + // var newWhere = new Dictionary(primary); + // foreach (var pair in secondary) + // { + // var condition = pair.Value as IDictionary; + // if (newWhere.ContainsKey(pair.Key)) + // { + // var oldCondition = newWhere[pair.Key] as IDictionary; + // if (oldCondition == null || condition == null) + // { + // throw new ArgumentException("More than one where clause for the given key provided."); + // } + // var newCondition = new Dictionary(oldCondition); + // foreach (var conditionPair in condition) + // { + // if (newCondition.ContainsKey(conditionPair.Key)) + // { + // throw new ArgumentException("More than one condition for the given key provided."); + // } + // newCondition[conditionPair.Key] = conditionPair.Value; + // } + // newWhere[pair.Key] = newCondition; + // } + // else + // { + // newWhere[pair.Key] = pair.Value; + // } + // } + // return newWhere; + // } + + // ///// + // ///// Constructs a query that is the or of the given queries. + // ///// + // ///// The list of AVQueries to 'or' together. + // ///// A AVQquery that is the 'or' of the passed in queries. + // //public static AVQuery Or(IEnumerable> queries) + // //{ + // // string className = null; + // // var orValue = new List>(); + // // // We need to cast it to non-generic IEnumerable because of AOT-limitation + // // var nonGenericQueries = (IEnumerable)queries; + // // foreach (var obj in nonGenericQueries) + // // { + // // var q = (AVQuery)obj; + // // if (className != null && q.className != className) + // // { + // // throw new ArgumentException( + // // "All of the queries in an or query must be on the same class."); + // // } + // // className = q.className; + // // var parameters = q.BuildParameters(); + // // if (parameters.Count == 0) + // // { + // // continue; + // // } + // // object where; + // // if (!parameters.TryGetValue("where", out where) || parameters.Count > 1) + // // { + // // throw new ArgumentException( + // // "None of the queries in an or query can have non-filtering clauses"); + // // } + // // orValue.Add(where as IDictionary); + // // } + // // return new AVQuery(new AVQuery(className), + // // where: new Dictionary { + // // {"$or", orValue} + // // }); + // //} + + // #region Order By + + // /// + // /// Sorts the results in ascending order by the given key. + // /// This will override any existing ordering for the query. + // /// + // /// The key to order by. + // /// A new query with the additional constraint. + // public virtual S OrderBy(string key) + // { + // return CreateInstance( replacementOrderBy: new List { key }); + // } + + // /// + // /// Sorts the results in descending order by the given key. + // /// This will override any existing ordering for the query. + // /// + // /// The key to order by. + // /// A new query with the additional constraint. + // public virtual S OrderByDescending(string key) + // { + // return CreateInstance( replacementOrderBy: new List { "-" + key }); + // } + + // /// + // /// Sorts the results in ascending order by the given key, after previous + // /// ordering has been applied. + // /// + // /// This method can only be called if there is already an + // /// or + // /// on this query. + // /// + // /// The key to order by. + // /// A new query with the additional constraint. + // public virtual S ThenBy(string key) + // { + // return CreateInstance( thenBy: new List { key }); + // } + + // /// + // /// Sorts the results in descending order by the given key, after previous + // /// ordering has been applied. + // /// + // /// This method can only be called if there is already an + // /// or on this query. + // /// + // /// The key to order by. + // /// A new query with the additional constraint. + // public virtual S ThenByDescending(string key) + // { + // return CreateInstance( thenBy: new List { "-" + key }); + // } + + // #endregion + + // /// + // /// Include nested AVObjects for the provided key. You can use dot notation + // /// to specify which fields in the included objects should also be fetched. + // /// + // /// The key that should be included. + // /// A new query with the additional constraint. + // public virtual S Include(string key) + // { + // return CreateInstance( includes: new List { key }); + // } + + // /// + // /// Restrict the fields of returned AVObjects to only include the provided key. + // /// If this is called multiple times, then all of the keys specified in each of + // /// the calls will be included. + // /// + // /// The key that should be included. + // /// A new query with the additional constraint. + // public virtual S Select(string key) + // { + // return CreateInstance( selectedKeys: new List { key }); + // } + + // /// + // /// Skips a number of results before returning. This is useful for pagination + // /// of large queries. Chaining multiple skips together will cause more results + // /// to be skipped. + // /// + // /// The number of results to skip. + // /// A new query with the additional constraint. + // public virtual S Skip(int count) + // { + // return CreateInstance( skip: count); + // } + + // /// + // /// Controls the maximum number of results that are returned. Setting a negative + // /// limit denotes retrieval without a limit. Chaining multiple limits + // /// results in the last limit specified being used. The default limit is + // /// 100, with a maximum of 1000 results being returned at a time. + // /// + // /// The maximum number of results to return. + // /// A new query with the additional constraint. + // public virtual S Limit(int count) + // { + // return CreateInstance( limit: count); + // } + + // internal virtual S RedirectClassName(String key) + // { + // return CreateInstance( redirectClassNameForKey: key); + // } + + // #region Where + + // /// + // /// Adds a constraint to the query that requires a particular key's value to be + // /// contained in the provided list of values. + // /// + // /// The key to check. + // /// The values that will match. + // /// A new query with the additional constraint. + // public virtual S WhereContainedIn(string key, IEnumerable values) + // { + // return CreateInstance( where: new Dictionary { + // { key, new Dictionary{{"$in", values.ToList()}}} + // }); + // } + + // /// + // /// Add a constraint to the querey that requires a particular key's value to be + // /// a list containing all of the elements in the provided list of values. + // /// + // /// The key to check. + // /// The values that will match. + // /// A new query with the additional constraint. + // public virtual S WhereContainsAll(string key, IEnumerable values) + // { + // return CreateInstance( where: new Dictionary { + // { key, new Dictionary{{"$all", values.ToList()}}} + // }); + // } + + // /// + // /// Adds a constraint for finding string values that contain a provided string. + // /// This will be slow for large data sets. + // /// + // /// The key that the string to match is stored in. + // /// The substring that the value must contain. + // /// A new query with the additional constraint. + // public virtual S WhereContains(string key, string substring) + // { + // return CreateInstance( where: new Dictionary { + // { key, new Dictionary{{"$regex", RegexQuote(substring)}}} + // }); + // } + + // /// + // /// Adds a constraint for finding objects that do not contain a given key. + // /// + // /// The key that should not exist. + // /// A new query with the additional constraint. + // public virtual S WhereDoesNotExist(string key) + // { + // return CreateInstance( where: new Dictionary{ + // { key, new Dictionary{{"$exists", false}}} + // }); + // } + + // /// + // /// Adds a constraint to the query that requires that a particular key's value + // /// does not match another AVQuery. This only works on keys whose values are + // /// AVObjects or lists of AVObjects. + // /// + // /// The key to check. + // /// The query that the value should not match. + // /// A new query with the additional constraint. + // public virtual S WhereDoesNotMatchQuery(string key, AVQuery query) + // where TOther : AVObject + // { + // return CreateInstance( where: new Dictionary { + // { key, new Dictionary{{"$notInQuery", query.BuildParameters(true)}}} + // }); + // } + + // /// + // /// Adds a constraint for finding string values that end with a provided string. + // /// This will be slow for large data sets. + // /// + // /// The key that the string to match is stored in. + // /// The substring that the value must end with. + // /// A new query with the additional constraint. + // public virtual S WhereEndsWith(string key, string suffix) + // { + // return CreateInstance( where: new Dictionary { + // { key, new Dictionary{{"$regex", RegexQuote(suffix) + "$"}}} + // }); + // } + + // /// + // /// Adds a constraint to the query that requires a particular key's value to be + // /// equal to the provided value. + // /// + // /// The key to check. + // /// The value that the AVObject must contain. + // /// A new query with the additional constraint. + // public virtual S WhereEqualTo(string key, object value) + // { + // return CreateInstance( where: new Dictionary { + // { key, value} + // }); + // } + + // /// + // /// Adds a constraint to the query that requires a particular key's size to be + // /// equal to the provided size. + // /// + // /// The size equal to. + // /// The key to check. + // /// The value that the size must be. + // /// A new query with the additional constraint. + // public virtual S WhereSizeEqualTo(string key, uint size) + // { + // return CreateInstance( where: new Dictionary { + // { key, new Dictionary{{"$size", size}}} + // }); + // } + + // /// + // /// Adds a constraint for finding objects that contain a given key. + // /// + // /// The key that should exist. + // /// A new query with the additional constraint. + // public virtual S WhereExists(string key) + // { + // return CreateInstance( where: new Dictionary{ + // { key, new Dictionary{{"$exists", true}}} + // }); + // } + + // /// + // /// Adds a constraint to the query that requires a particular key's value to be + // /// greater than the provided value. + // /// + // /// The key to check. + // /// The value that provides a lower bound. + // /// A new query with the additional constraint. + // public virtual S WhereGreaterThan(string key, object value) + // { + // return CreateInstance( where: new Dictionary{ + // { key, new Dictionary{{"$gt", value}}} + // }); + // } + + // /// + // /// Adds a constraint to the query that requires a particular key's value to be + // /// greater or equal to than the provided value. + // /// + // /// The key to check. + // /// The value that provides a lower bound. + // /// A new query with the additional constraint. + // public virtual S WhereGreaterThanOrEqualTo(string key, object value) + // { + // return CreateInstance( where: new Dictionary{ + // { key, new Dictionary{{"$gte", value}}} + // }); + // } + + // /// + // /// Adds a constraint to the query that requires a particular key's value to be + // /// less than the provided value. + // /// + // /// The key to check. + // /// The value that provides an upper bound. + // /// A new query with the additional constraint. + // public virtual S WhereLessThan(string key, object value) + // { + // return CreateInstance( where: new Dictionary{ + // { key, new Dictionary{{"$lt", value}}} + // }); + // } + + // /// + // /// Adds a constraint to the query that requires a particular key's value to be + // /// less than or equal to the provided value. + // /// + // /// The key to check. + // /// The value that provides a lower bound. + // /// A new query with the additional constraint. + // public virtual S WhereLessThanOrEqualTo(string key, object value) + // { + // return CreateInstance( where: new Dictionary{ + // { key, new Dictionary{{"$lte", value}}} + // }); + // } + + // /// + // /// Adds a regular expression constraint for finding string values that match the provided + // /// regular expression. This may be slow for large data sets. + // /// + // /// The key that the string to match is stored in. + // /// The regular expression pattern to match. The Regex must + // /// have the options flag set. + // /// Any of the following supported PCRE modifiers: + // /// i - Case insensitive search + // /// m Search across multiple lines of input + // /// A new query with the additional constraint. + // public virtual S WhereMatches(string key, Regex regex, string modifiers) + // { + // if (!regex.Options.HasFlag(RegexOptions.ECMAScript)) + // { + // throw new ArgumentException( + // "Only ECMAScript-compatible regexes are supported. Please use the ECMAScript RegexOptions flag when creating your regex."); + // } + // return CreateInstance( where: new Dictionary { + // { key, EncodeRegex(regex, modifiers)} + // }); + // } + + // /// + // /// Adds a regular expression constraint for finding string values that match the provided + // /// regular expression. This may be slow for large data sets. + // /// + // /// The key that the string to match is stored in. + // /// The regular expression pattern to match. The Regex must + // /// have the options flag set. + // /// A new query with the additional constraint. + // public virtual S WhereMatches(string key, Regex regex) + // { + // return WhereMatches(key, regex, null); + // } + + // /// + // /// Adds a regular expression constraint for finding string values that match the provided + // /// regular expression. This may be slow for large data sets. + // /// + // /// The key that the string to match is stored in. + // /// The PCRE regular expression pattern to match. + // /// Any of the following supported PCRE modifiers: + // /// i - Case insensitive search + // /// m Search across multiple lines of input + // /// A new query with the additional constraint. + // public virtual S WhereMatches(string key, string pattern, string modifiers = null) + // { + // return WhereMatches(key, new Regex(pattern, RegexOptions.ECMAScript), modifiers); + // } + + // /// + // /// Adds a regular expression constraint for finding string values that match the provided + // /// regular expression. This may be slow for large data sets. + // /// + // /// The key that the string to match is stored in. + // /// The PCRE regular expression pattern to match. + // /// A new query with the additional constraint. + // public virtual S WhereMatches(string key, string pattern) + // { + // return WhereMatches(key, pattern, null); + // } + + // /// + // /// Adds a constraint to the query that requires a particular key's value + // /// to match a value for a key in the results of another AVQuery. + // /// + // /// The key whose value is being checked. + // /// The key in the objects from the subquery to look in. + // /// The subquery to run + // /// A new query with the additional constraint. + // public virtual S WhereMatchesKeyInQuery(string key, + // string keyInQuery, + // AVQuery query) where TOther : AVObject + // { + // var parameters = new Dictionary { + // { "query", query.BuildParameters(true)}, + // { "key", keyInQuery} + // }; + // return CreateInstance( where: new Dictionary { + // { key, new Dictionary{{"$select", parameters}}} + // }); + // } + + // /// + // /// Adds a constraint to the query that requires a particular key's value + // /// does not match any value for a key in the results of another AVQuery. + // /// + // /// The key whose value is being checked. + // /// The key in the objects from the subquery to look in. + // /// The subquery to run + // /// A new query with the additional constraint. + // public virtual S WhereDoesNotMatchesKeyInQuery(string key, + // string keyInQuery, + // AVQuery query) where TOther : AVObject + // { + // var parameters = new Dictionary { + // { "query", query.BuildParameters(true)}, + // { "key", keyInQuery} + // }; + // return CreateInstance( where: new Dictionary { + // { key, new Dictionary{{"$dontSelect", parameters}}} + // }); + // } + + // /// + // /// Adds a constraint to the query that requires that a particular key's value + // /// matches another AVQuery. This only works on keys whose values are + // /// AVObjects or lists of AVObjects. + // /// + // /// The key to check. + // /// The query that the value should match. + // /// A new query with the additional constraint. + // public virtual S WhereMatchesQuery(string key, AVQuery query) + // where TOther : AVObject + // { + // return CreateInstance( where: new Dictionary { + // { key, new Dictionary{{"$inQuery", query.BuildParameters(true)}}} + // }); + // } + + // /// + // /// Adds a proximity-based constraint for finding objects with keys whose GeoPoint + // /// values are near the given point. + // /// + // /// The key that the AVGeoPoint is stored in. + // /// The reference AVGeoPoint. + // /// A new query with the additional constraint. + // public virtual S WhereNear(string key, AVGeoPoint point) + // { + // return CreateInstance( where: new Dictionary { + // { key, new Dictionary{{"$nearSphere", point}}} + // }); + // } + + // /// + // /// Adds a constraint to the query that requires a particular key's value to be + // /// contained in the provided list of values. + // /// + // /// The key to check. + // /// The values that will match. + // /// A new query with the additional constraint. + // public virtual S WhereNotContainedIn(string key, IEnumerable values) + // { + // return CreateInstance( where: new Dictionary { + // { key, new Dictionary{{"$nin", values.ToList()}}} + // }); + // } + + // /// + // /// Adds a constraint to the query that requires a particular key's value not + // /// to be equal to the provided value. + // /// + // /// The key to check. + // /// The value that that must not be equalled. + // /// A new query with the additional constraint. + // public virtual S WhereNotEqualTo(string key, object value) + // { + // return CreateInstance( where: new Dictionary { + // { key, new Dictionary{{"$ne", value}}} + // }); + // } + + // /// + // /// Adds a constraint for finding string values that start with the provided string. + // /// This query will use the backend index, so it will be fast even with large data sets. + // /// + // /// The key that the string to match is stored in. + // /// The substring that the value must start with. + // /// A new query with the additional constraint. + // public virtual S WhereStartsWith(string key, string suffix) + // { + // return CreateInstance( where: new Dictionary { + // { key, new Dictionary{{"$regex", "^" + RegexQuote(suffix)}}} + // }); + // } + + // /// + // /// Add a constraint to the query that requires a particular key's coordinates to be + // /// contained within a given rectangular geographic bounding box. + // /// + // /// The key to be constrained. + // /// The lower-left inclusive corner of the box. + // /// The upper-right inclusive corner of the box. + // /// A new query with the additional constraint. + // public virtual S WhereWithinGeoBox(string key, + // AVGeoPoint southwest, + // AVGeoPoint northeast) + // { + + // return this.CreateInstance( where: new Dictionary + // { + // { + // key, + // new Dictionary + // { + // { + // "$within", + // new Dictionary { + // { "$box", new[] {southwest, northeast}} + // } + // } + // } + // } + // }); + // } + + // /// + // /// Adds a proximity-based constraint for finding objects with keys whose GeoPoint + // /// values are near the given point and within the maximum distance given. + // /// + // /// The key that the AVGeoPoint is stored in. + // /// The reference AVGeoPoint. + // /// The maximum distance (in radians) of results to return. + // /// A new query with the additional constraint. + // public virtual S WhereWithinDistance( + // string key, AVGeoPoint point, AVGeoDistance maxDistance) + // { + // var nearWhere = new Dictionary { + // { key, new Dictionary{{"$nearSphere", point}}} + // }; + // var mergedWhere = MergeWhere(nearWhere, new Dictionary { + // { key, new Dictionary{{"$maxDistance", maxDistance.Radians}}} + // }); + // return CreateInstance( where: mergedWhere); + // } + + // internal virtual S WhereRelatedTo(AVObject parent, string key) + // { + // return CreateInstance( where: new Dictionary { + // { + // "$relatedTo", + // new Dictionary { + // { "object", parent}, + // { "key", key} + // } + // } + // }); + // } + + // #endregion + + // /// + // /// Retrieves a list of AVObjects that satisfy this query from LeanCloud. + // /// + // /// The list of AVObjects that match this query. + // public virtual Task> FindAsync() + // { + // return FindAsync(CancellationToken.None); + // } + + // /// + // /// Retrieves a list of AVObjects that satisfy this query from LeanCloud. + // /// + // /// The cancellation token. + // /// The list of AVObjects that match this query. + // public abstract Task> FindAsync(CancellationToken cancellationToken); + + + // /// + // /// Retrieves at most one AVObject that satisfies this query. + // /// + // /// A single AVObject that satisfies this query, or else null. + // public virtual Task FirstOrDefaultAsync() + // { + // return FirstOrDefaultAsync(CancellationToken.None); + // } + + // /// + // /// Retrieves at most one AVObject that satisfies this query. + // /// + // /// The cancellation token. + // /// A single AVObject that satisfies this query, or else null. + // public abstract Task FirstOrDefaultAsync(CancellationToken cancellationToken); + + // /// + // /// Retrieves at most one AVObject that satisfies this query. + // /// + // /// A single AVObject that satisfies this query. + // /// If no results match the query. + // public virtual Task FirstAsync() + // { + // return FirstAsync(CancellationToken.None); + // } + + // /// + // /// Retrieves at most one AVObject that satisfies this query. + // /// + // /// The cancellation token. + // /// A single AVObject that satisfies this query. + // /// If no results match the query. + // public abstract Task FirstAsync(CancellationToken cancellationToken); + + // /// + // /// Counts the number of objects that match this query. + // /// + // /// The number of objects that match this query. + // public virtual Task CountAsync() + // { + // return CountAsync(CancellationToken.None); + // } + + // /// + // /// Counts the number of objects that match this query. + // /// + // /// The cancellation token. + // /// The number of objects that match this query. + // public abstract Task CountAsync(CancellationToken cancellationToken); + + // /// + // /// Constructs a AVObject whose id is already known by fetching data + // /// from the server. + // /// + // /// ObjectId of the AVObject to fetch. + // /// The AVObject for the given objectId. + // public virtual Task GetAsync(string objectId) + // { + // return GetAsync(objectId, CancellationToken.None); + // } + + // /// + // /// Constructs a AVObject whose id is already known by fetching data + // /// from the server. + // /// + // /// ObjectId of the AVObject to fetch. + // /// The cancellation token. + // /// The AVObject for the given objectId. + // public abstract Task GetAsync(string objectId, CancellationToken cancellationToken); + + // internal object GetConstraint(string key) + // { + // return where == null ? null : where.GetOrDefault(key, null); + // } + + // /// + // /// 构建查询字符串 + // /// + // /// 是否包含 ClassName + // /// + // public IDictionary BuildParameters(bool includeClassName = false) + // { + // Dictionary result = new Dictionary(); + // if (where != null) + // { + // result["where"] = PointerOrLocalIdEncoder.Instance.Encode(where); + // } + // if (orderBy != null) + // { + // result["order"] = string.Join(",", orderBy.ToArray()); + // } + // if (skip != null) + // { + // result["skip"] = skip.Value; + // } + // if (limit != null) + // { + // result["limit"] = limit.Value; + // } + // if (includes != null) + // { + // result["include"] = string.Join(",", includes.ToArray()); + // } + // if (selectedKeys != null) + // { + // result["keys"] = string.Join(",", selectedKeys.ToArray()); + // } + // if (includeClassName) + // { + // result["className"] = className; + // } + // if (redirectClassNameForKey != null) + // { + // result["redirectClassNameForKey"] = redirectClassNameForKey; + // } + // return result; + // } + + // private string RegexQuote(string input) + // { + // return "\\Q" + input.Replace("\\E", "\\E\\\\E\\Q") + "\\E"; + // } + + // private string GetRegexOptions(Regex regex, string modifiers) + // { + // string result = modifiers ?? ""; + // if (regex.Options.HasFlag(RegexOptions.IgnoreCase) && !modifiers.Contains("i")) + // { + // result += "i"; + // } + // if (regex.Options.HasFlag(RegexOptions.Multiline) && !modifiers.Contains("m")) + // { + // result += "m"; + // } + // return result; + // } + + // private IDictionary EncodeRegex(Regex regex, string modifiers) + // { + // var options = GetRegexOptions(regex, modifiers); + // var dict = new Dictionary(); + // dict["$regex"] = regex.ToString(); + // if (!string.IsNullOrEmpty(options)) + // { + // dict["$options"] = options; + // } + // return dict; + // } + + // /// + // /// Serves as the default hash function. + // /// + // /// A hash code for the current object. + // public override int GetHashCode() + // { + // // TODO (richardross): Implement this. + // return 0; + // } + //} +} diff --git a/Storage/Source/Public/LeaderBoard/AVLeaderboard.cs b/Storage/Source/Public/LeaderBoard/AVLeaderboard.cs new file mode 100644 index 0000000..8713c1b --- /dev/null +++ b/Storage/Source/Public/LeaderBoard/AVLeaderboard.cs @@ -0,0 +1,479 @@ +using System; +using System.Threading.Tasks; +using System.Collections.Generic; +using LeanCloud.Storage.Internal; +using System.IO; +using System.Text; + +namespace LeanCloud { + /// + /// 排行榜顺序 + /// + public enum AVLeaderboardOrder { + /// + /// 升序 + /// + ASCENDING, + /// + /// 降序 + /// + DESCENDING + } + + /// + /// 排行榜更新策略 + /// + public enum AVLeaderboardUpdateStrategy { + /// + /// 更好地 + /// + BETTER, + /// + /// 最近的 + /// + LAST, + /// + /// 总和 + /// + SUM, + } + + /// + /// 排行榜刷新频率 + /// + public enum AVLeaderboardVersionChangeInterval { + /// + /// 从不 + /// + NEVER, + /// + /// 每天 + /// + DAY, + /// + /// 每周 + /// + WEEK, + /// + /// 每月 + /// + MONTH + } + + /// + /// 排行榜类 + /// + public class AVLeaderboard { + /// + /// 成绩名字 + /// + /// The name of the statistic. + public string StatisticName { + get; private set; + } + + /// + /// 排行榜顺序 + /// + /// The order. + public AVLeaderboardOrder Order { + get; private set; + } + + /// + /// 排行榜更新策略 + /// + /// The update strategy. + public AVLeaderboardUpdateStrategy UpdateStrategy { + get; private set; + } + + /// + /// 排行榜版本更新频率 + /// + /// The version change intervak. + public AVLeaderboardVersionChangeInterval VersionChangeInterval { + get; private set; + } + + /// + /// 版本号 + /// + /// The version. + public int Version { + get; private set; + } + + /// + /// 下次重置时间 + /// + /// The next reset at. + public DateTime NextResetAt { + get; private set; + } + + /// + /// 创建时间 + /// + /// The created at. + public DateTime CreatedAt { + get; private set; + } + + /// + /// Leaderboard 构造方法 + /// + /// 成绩名称 + AVLeaderboard(string statisticName) { + StatisticName = statisticName; + } + + AVLeaderboard() { + } + + /// + /// 创建排行榜对象 + /// + /// 排行榜对象 + /// 名称 + /// 排序方式 + /// 版本更新频率 + /// 成绩更新策略 + public static Task CreateLeaderboard(string statisticName, + AVLeaderboardOrder order = AVLeaderboardOrder.DESCENDING, + AVLeaderboardUpdateStrategy updateStrategy = AVLeaderboardUpdateStrategy.BETTER, + AVLeaderboardVersionChangeInterval versionChangeInterval = AVLeaderboardVersionChangeInterval.WEEK) { + + if (string.IsNullOrEmpty(statisticName)) { + throw new ArgumentNullException(nameof(statisticName)); + } + var data = new Dictionary { + { "statisticName", statisticName }, + { "order", order.ToString().ToLower() }, + { "versionChangeInterval", versionChangeInterval.ToString().ToLower() }, + { "updateStrategy", updateStrategy.ToString().ToLower() }, + }; + var command = new AVCommand("leaderboard/leaderboards", "POST", data: data); + return AVPlugins.Instance.CommandRunner.RunCommandAsync(command).OnSuccess(t => { + try { + var leaderboard = Parse(t.Result.Item2); + return leaderboard; + } catch (Exception e) { + throw new AVException(AVException.ErrorCode.InvalidJSON, e.Message); + } + }); + } + + /// + /// 创建排行榜对象 + /// + /// 排行榜对象 + /// 名称 + public static AVLeaderboard CreateWithoutData(string statisticName) { + if (string.IsNullOrEmpty(statisticName)) { + throw new ArgumentNullException(nameof(statisticName)); + } + return new AVLeaderboard(statisticName); + } + + /// + /// 获取排行榜对象 + /// + /// 排行榜对象 + /// 名称 + public static Task GetLeaderboard(string statisticName) { + return CreateWithoutData(statisticName).Fetch(); + } + + /// + /// 更新用户成绩 + /// + /// 更新的成绩 + /// 用户 + /// 成绩 + /// 是否强行覆盖 + public static Task> UpdateStatistics(AVUser user, Dictionary statistics, bool overwrite = false) { + if (user == null) { + throw new ArgumentNullException(nameof(user)); + } + if (statistics == null || statistics.Count == 0) { + throw new ArgumentNullException(nameof(statistics)); + } + var data = new List(); + foreach (var statistic in statistics) { + var kv = new Dictionary { + { "statisticName", statistic.Key }, + { "statisticValue", statistic.Value }, + }; + data.Add(kv); + } + var path = string.Format("leaderboard/users/{0}/statistics", user.ObjectId); + if (overwrite) { + path = string.Format("{0}?overwrite=1", path); + } + var dataStr = Json.Encode(data); + var dataStream = new MemoryStream(Encoding.UTF8.GetBytes(dataStr)); + var command = new AVCommand(path, "POST", contentType: "application/json", sessionToken: user.SessionToken, stream: dataStream); + return AVPlugins.Instance.CommandRunner.RunCommandAsync(command).OnSuccess(t => { + try { + List statisticList = new List(); + List list = t.Result.Item2["results"] as List; + foreach (object obj in list) { + statisticList.Add(AVStatistic.Parse(obj as IDictionary)); + } + return statisticList; + } catch (Exception e) { + throw new AVException(AVException.ErrorCode.InvalidJSON, e.Message); + } + }); + } + + /// + /// 获取用户成绩 + /// + /// 成绩列表 + /// 用户 + /// 名称列表 + public static Task> GetStatistics(AVUser user, List statisticNames = null) { + if (user == null) { + throw new ArgumentNullException(nameof(user)); + } + var path = string.Format("leaderboard/users/{0}/statistics", user.ObjectId); + if (statisticNames != null && statisticNames.Count > 0) { + var names = string.Join(",", statisticNames.ToArray()); + path = string.Format("{0}?statistics={1}", path, names); + } + var sessionToken = AVUser.CurrentUser?.SessionToken; + var command = new AVCommand(path, "GET", sessionToken, data: null); + return AVPlugins.Instance.CommandRunner.RunCommandAsync(command).OnSuccess(t => { + try { + List statisticList = new List(); + List list = t.Result.Item2["results"] as List; + foreach (object obj in list) { + statisticList.Add(AVStatistic.Parse(obj as IDictionary)); + } + return statisticList; + } catch (Exception e) { + throw new AVException(AVException.ErrorCode.InvalidJSON, e.Message); + } + }); + } + + /// + /// 删除用户成绩 + /// + /// 用户 + /// 名称列表 + public static Task DeleteStatistics(AVUser user, List statisticNames) { + if (user == null) { + throw new ArgumentNullException(nameof(user)); + } + if (statisticNames == null || statisticNames.Count == 0) { + throw new ArgumentNullException(nameof(statisticNames)); + } + var path = string.Format("leaderboard/users/{0}/statistics", user.ObjectId); + var names = string.Join(",", statisticNames.ToArray()); + path = string.Format("{0}?statistics={1}", path, names); + var command = new AVCommand(path, "DELETE", sessionToken: user.SessionToken, data: null); + return AVPlugins.Instance.CommandRunner.RunCommandAsync(command); + } + + /// + /// 获取排行榜历史数据 + /// + /// 排行榜归档列表 + /// 跳过数量 + /// 分页数量 + public Task> GetArchives(int skip = 0, int limit = 10) { + var path = string.Format("leaderboard/leaderboards/{0}/archives", StatisticName); + path = string.Format("{0}?skip={1}&limit={2}", path, skip, limit); + var command = new AVCommand(path, "GET", data: null); + return AVPlugins.Instance.CommandRunner.RunCommandAsync(command).OnSuccess(t => { + List archives = new List(); + List list = t.Result.Item2["results"] as List; + foreach (object obj in list) { + archives.Add(AVLeaderboardArchive.Parse(obj as IDictionary)); + } + return archives; + }); + } + + /// + /// 获取排行榜结果 + /// + /// 排名列表 + public Task> GetResults(int version = -1, int skip = 0, int limit = 10, List selectUserKeys = null, + List includeStatistics = null) { + return GetResults(null, version, skip, limit, selectUserKeys, includeStatistics); + } + + /// + /// 获取用户及附近的排名 + /// + /// 排名列表 + /// 用户 + /// 版本号 + /// 跳过数量 + /// 分页数量 + /// 包含的玩家的字段列表 + /// 包含的其他排行榜名称 + public Task> GetResultsAroundUser(int version = -1, int skip = 0, int limit = 10, + List selectUserKeys = null, + List includeStatistics = null) { + return GetResults(AVUser.CurrentUser, version, skip, limit, selectUserKeys, includeStatistics); + } + + Task> GetResults(AVUser user, + int version, int skip, int limit, + List selectUserKeys, + List includeStatistics) { + + var path = string.Format("leaderboard/leaderboards/{0}/ranks", StatisticName); + if (user != null) { + path = string.Format("{0}/{1}", path, user.ObjectId); + } + path = string.Format("{0}?skip={1}&limit={2}", path, skip, limit); + if (version != -1) { + path = string.Format("{0}&version={1}", path, version); + } + if (selectUserKeys != null) { + var keys = string.Join(",", selectUserKeys.ToArray()); + path = string.Format("{0}&includeUser={1}", path, keys); + } + if (includeStatistics != null) { + var statistics = string.Join(",", includeStatistics.ToArray()); + path = string.Format("{0}&includeStatistics={1}", path, statistics); + } + var command = new AVCommand(path, "GET", data: null); + return AVPlugins.Instance.CommandRunner.RunCommandAsync(command).OnSuccess(t => { + try { + List rankingList = new List(); + List list = t.Result.Item2["results"] as List; + foreach (object obj in list) { + rankingList.Add(AVRanking.Parse(obj as IDictionary)); + } + return rankingList; + } catch (Exception e) { + throw new AVException(AVException.ErrorCode.InvalidJSON, e.Message); + } + }); + } + + /// + /// 设置更新策略 + /// + /// 排行榜对象 + /// 更新策略 + public Task UpdateUpdateStrategy(AVLeaderboardUpdateStrategy updateStrategy) { + var data = new Dictionary { + { "updateStrategy", updateStrategy.ToString().ToLower() } + }; + return Update(data).OnSuccess(t => { + UpdateStrategy = (AVLeaderboardUpdateStrategy)Enum.Parse(typeof(AVLeaderboardUpdateStrategy), t.Result["updateStrategy"].ToString().ToUpper()); + return this; + }); + } + + /// + /// 设置版本更新频率 + /// + /// 排行榜对象 + /// 版本更新频率 + public Task UpdateVersionChangeInterval(AVLeaderboardVersionChangeInterval versionChangeInterval) { + var data = new Dictionary { + { "versionChangeInterval", versionChangeInterval.ToString().ToLower() } + }; + return Update(data).OnSuccess(t => { + VersionChangeInterval = (AVLeaderboardVersionChangeInterval)Enum.Parse(typeof(AVLeaderboardVersionChangeInterval), t.Result["versionChangeInterval"].ToString().ToUpper()); + return this; + }); + } + + Task> Update(Dictionary data) { + var path = string.Format("leaderboard/leaderboards/{0}", StatisticName); + var command = new AVCommand(path, "PUT", data: data); + return AVPlugins.Instance.CommandRunner.RunCommandAsync(command).OnSuccess(t => { + return t.Result.Item2; + }); + } + + /// + /// 拉取排行榜数据 + /// + /// 排行榜对象 + public Task Fetch() { + var path = string.Format("leaderboard/leaderboards/{0}", StatisticName); + var command = new AVCommand(path, "GET", data: null); + return AVPlugins.Instance.CommandRunner.RunCommandAsync(command).OnSuccess(t => { + try { + // 反序列化 Leaderboard 对象 + var leaderboard = Parse(t.Result.Item2); + return leaderboard; + } catch (Exception e) { + throw new AVException(AVException.ErrorCode.InvalidJSON, e.Message); + } + }); + } + + /// + /// 重置排行榜 + /// + /// 排行榜对象 + public Task Reset() { + var path = string.Format("leaderboard/leaderboards/{0}/incrementVersion", StatisticName); + var command = new AVCommand(path, "PUT", data: null); + return AVPlugins.Instance.CommandRunner.RunCommandAsync(command).OnSuccess(t => { + try { + Init(t.Result.Item2); + return this; + } catch (Exception e) { + throw new AVException(AVException.ErrorCode.InvalidJSON, e.Message); + } + }); + } + + /// + /// 销毁排行榜 + /// + public Task Destroy() { + var path = string.Format("leaderboard/leaderboards/{0}", StatisticName); + var command = new AVCommand(path, "DELETE", data: null); + return AVPlugins.Instance.CommandRunner.RunCommandAsync(command); + } + + static AVLeaderboard Parse(IDictionary data) { + if (data == null) { + throw new ArgumentNullException(nameof(data)); + } + var leaderboard = new AVLeaderboard(); + leaderboard.Init(data); + return leaderboard; + } + + void Init(IDictionary data) { + if (data == null) { + throw new ArgumentNullException(nameof(data)); + } + object nameObj; + if (data.TryGetValue("statisticName", out nameObj)) { + StatisticName = nameObj.ToString(); + } + object orderObj; + if (data.TryGetValue("order", out orderObj)) { + Order = (AVLeaderboardOrder)Enum.Parse(typeof(AVLeaderboardOrder), orderObj.ToString().ToUpper()); + } + object strategyObj; + if (data.TryGetValue("updateStrategy", out strategyObj)) { + UpdateStrategy = (AVLeaderboardUpdateStrategy)Enum.Parse(typeof(AVLeaderboardUpdateStrategy), strategyObj.ToString().ToUpper()); + } + object intervalObj; + if (data.TryGetValue("versionChangeInterval", out intervalObj)) { + VersionChangeInterval = (AVLeaderboardVersionChangeInterval)Enum.Parse(typeof(AVLeaderboardVersionChangeInterval), intervalObj.ToString().ToUpper()); + } + object versionObj; + if (data.TryGetValue("version", out versionObj)) { + Version = int.Parse(versionObj.ToString()); + } + } + } +} diff --git a/Storage/Source/Public/LeaderBoard/AVLeaderboardArchive.cs b/Storage/Source/Public/LeaderBoard/AVLeaderboardArchive.cs new file mode 100644 index 0000000..a9df41f --- /dev/null +++ b/Storage/Source/Public/LeaderBoard/AVLeaderboardArchive.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using LeanCloud.Storage.Internal; + +namespace LeanCloud { + /// + /// 归档的排行榜 + /// + public class AVLeaderboardArchive { + /// + /// 名称 + /// + /// The name of the statistic. + public string StatisticName { + get; internal set; + } + + /// + /// 版本号 + /// + /// The version. + public int Version { + get; internal set; + } + + /// + /// 状态 + /// + /// The status. + public string Status { + get; internal set; + } + + /// + /// 下载地址 + /// + /// The URL. + public string Url { + get; internal set; + } + + /// + /// 激活时间 + /// + /// The activated at. + public DateTime ActivatedAt { + get; internal set; + } + + /// + /// 归档时间 + /// + /// The deactivated at. + public DateTime DeactivatedAt { + get; internal set; + } + + AVLeaderboardArchive() { + } + + internal static AVLeaderboardArchive Parse(IDictionary data) { + if (data == null) { + throw new ArgumentNullException(nameof(data)); + } + AVLeaderboardArchive archive = new AVLeaderboardArchive { + StatisticName = data["statisticName"].ToString(), + Version = int.Parse(data["version"].ToString()), + Status = data["status"].ToString(), + Url = data["url"].ToString(), + ActivatedAt = (DateTime)AVDecoder.Instance.Decode(data["activatedAt"]), + DeactivatedAt = (DateTime)AVDecoder.Instance.Decode(data["activatedAt"]) + }; + return archive; + } + } +} diff --git a/Storage/Source/Public/LeaderBoard/AVRanking.cs b/Storage/Source/Public/LeaderBoard/AVRanking.cs new file mode 100644 index 0000000..6d5fa90 --- /dev/null +++ b/Storage/Source/Public/LeaderBoard/AVRanking.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using LeanCloud.Storage.Internal; + +namespace LeanCloud { + /// + /// 排名类 + /// + public class AVRanking { + /// + /// 名次 + /// + /// The rank. + public int Rank { + get; private set; + } + + /// + /// 用户 + /// + /// The user. + public AVUser User { + get; private set; + } + + public string StatisticName { + get; private set; + } + + /// + /// 分数 + /// + /// The value. + public double Value { + get; private set; + } + + /// + /// 成绩 + /// + /// The included statistics. + public List IncludedStatistics { + get; private set; + } + + AVRanking() { + } + + internal static AVRanking Parse(IDictionary data) { + if (data == null) { + throw new ArgumentNullException(nameof(data)); + } + var ranking = new AVRanking { + Rank = int.Parse(data["rank"].ToString()), + User = AVDecoder.Instance.Decode(data["user"]) as AVUser, + StatisticName = data["statisticName"].ToString(), + Value = double.Parse(data["statisticValue"].ToString()) + }; + object statisticsObj; + if (data.TryGetValue("statistics", out statisticsObj)) { + ranking.IncludedStatistics = new List(); + var statisticsObjList = statisticsObj as List; + foreach (object statisticObj in statisticsObjList) { + var statistic = AVStatistic.Parse(statisticObj as IDictionary); + ranking.IncludedStatistics.Add(statistic); + } + } + + return ranking; + } + } +} diff --git a/Storage/Source/Public/LeaderBoard/AVStatistic.cs b/Storage/Source/Public/LeaderBoard/AVStatistic.cs new file mode 100644 index 0000000..6d4fdcb --- /dev/null +++ b/Storage/Source/Public/LeaderBoard/AVStatistic.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; + +namespace LeanCloud { + /// + /// 成绩类 + /// + public class AVStatistic { + /// + /// 排行榜名称 + /// + /// The name. + public string Name { + get; private set; + } + + /// + /// 成绩值 + /// + /// The value. + public double Value { + get; private set; + } + + /// + /// 排行榜版本 + /// + /// The version. + public int Version { + get; internal set; + } + + public AVStatistic(string name, double value) { + Name = name; + Value = value; + } + + AVStatistic() { } + + internal static AVStatistic Parse(IDictionary data) { + if (data == null) { + throw new ArgumentNullException(nameof(data)); + } + AVStatistic statistic = new AVStatistic { + Name = data["statisticName"].ToString(), + Value = double.Parse(data["statisticValue"].ToString()), + Version = int.Parse(data["version"].ToString()) + }; + return statistic; + } + } +} diff --git a/Storage/Source/Public/Unity/AVInitializeBehaviour.cs b/Storage/Source/Public/Unity/AVInitializeBehaviour.cs new file mode 100644 index 0000000..1012c30 --- /dev/null +++ b/Storage/Source/Public/Unity/AVInitializeBehaviour.cs @@ -0,0 +1,75 @@ +using LeanCloud.Storage.Internal; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using UnityEngine; + +namespace LeanCloud +{ + public class AVCoreExtensions : MonoBehaviour + { + [RuntimeInitializeOnLoadMethod] + static void OnRuntimeMethodLoad() { + var go = new GameObject { + name = "AVCoreExtensions" + }; + DontDestroyOnLoad(go); + var avce = go.AddComponent(); + + Dispatcher.Instance.GameObject = go; + + // Kick off the dispatcher. + avce.StartCoroutine(Dispatcher.Instance.DispatcherCoroutine); + + AVInitializeBehaviour.IsWebPlayer = Application.platform == RuntimePlatform.WebGLPlayer; + } + } + + /// + /// Mandatory MonoBehaviour for scenes that use LeanCloud. Set the application ID and .NET key + /// in the editor. + /// + // TODO (hallucinogen): somehow because of Push, we need this class to be added in a GameObject + // called `AVInitializeBehaviour`. We might want to fix this. + public class AVInitializeBehaviour : MonoBehaviour + { + [SerializeField] + /// + /// The LeanCloud applicationId used in this app. You can get this value from the LeanCloud website. + /// + public string applicationID; + + + [SerializeField] + /// + /// The LeanCloud applicationKey used in this app. You can get this value from the LeanCloud website. + /// + public string applicationKey; + + [SerializeField] + /// + /// The service region. + /// + public AVClient.Configuration.AVRegion region; + + + [SerializeField] + /// + /// Use this uri as cloud function server host. This is used for local development. + /// + public string engineServer; + + [SerializeField] + /// + /// Whether use production stage to process request or not. + /// + public bool useProduction = true; + + /// + /// Gets or sets a value indicating whether this is web player. + /// + /// true if is web player; otherwise, false. + public static bool IsWebPlayer { get; set; } + } +} diff --git a/Storage/Source/Public/Utilities/Conversion.cs b/Storage/Source/Public/Utilities/Conversion.cs new file mode 100644 index 0000000..f802f5f --- /dev/null +++ b/Storage/Source/Public/Utilities/Conversion.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; +using LeanCloud.Storage.Internal; + +namespace LeanCloud.Utilities +{ + /// + /// A set of utilities for converting generic types between each other. + /// + public static class Conversion + { + /// + /// Converts a value to the requested type -- coercing primitives to + /// the desired type, wrapping lists and dictionaries appropriately, + /// or else returning null. + /// + /// This should be used on any containers that might be coming from a + /// user to normalize the collection types. Collection types coming from + /// JSON deserialization can be safely assumed to be lists or dictionaries of + /// objects. + /// + public static T As(object value) where T : class + { + return ConvertTo(value) as T; + } + + /// + /// Converts a value to the requested type -- coercing primitives to + /// the desired type, wrapping lists and dictionaries appropriately, + /// or else throwing an exception. + /// + /// This should be used on any containers that might be coming from a + /// user to normalize the collection types. Collection types coming from + /// JSON deserialization can be safely assumed to be lists or dictionaries of + /// objects. + /// + public static T To(object value) + { + return (T)ConvertTo(value); + } + + /// + /// Converts a value to the requested type -- coercing primitives to + /// the desired type, wrapping lists and dictionaries appropriately, + /// or else passing the object along to the caller unchanged. + /// + /// This should be used on any containers that might be coming from a + /// user to normalize the collection types. Collection types coming from + /// JSON deserialization can be safely assumed to be lists or dictionaries of + /// objects. + /// + internal static object ConvertTo(object value) + { + if (value is T || value == null) + { + return value; + } + + if (ReflectionHelpers.IsPrimitive(typeof(T))) + { + return (T)Convert.ChangeType(value, typeof(T)); + } + + if (ReflectionHelpers.IsConstructedGenericType(typeof(T))) + { + // Add lifting for nullables. Only supports conversions between primitives. + if (ReflectionHelpers.IsNullable(typeof(T))) + { + var innerType = ReflectionHelpers.GetGenericTypeArguments(typeof(T))[0]; + if (ReflectionHelpers.IsPrimitive(innerType)) + { + return (T)Convert.ChangeType(value, innerType); + } + } + Type listType = GetInterfaceType(value.GetType(), typeof(IList<>)); + var la = typeof(T).GetGenericTypeDefinition(); + var ilb = typeof(IList<>); + var lb = typeof(List<>); + if (listType != null && + (la == ilb || la == lb)) + { + var wrapperType = typeof(FlexibleListWrapper<,>) + .MakeGenericType(ReflectionHelpers.GetGenericTypeArguments(typeof(T))[0], + ReflectionHelpers.GetGenericTypeArguments(listType)[0]); + return Activator.CreateInstance(wrapperType, value); + } + Type dictType = GetInterfaceType(value.GetType(), typeof(IDictionary<,>)); + var da = typeof(T).GetGenericTypeDefinition(); + var db = typeof(IDictionary<,>); + if (dictType != null && + da == db) + { + var wrapperType = typeof(FlexibleDictionaryWrapper<,>) + .MakeGenericType(ReflectionHelpers.GetGenericTypeArguments(typeof(T))[1], + ReflectionHelpers.GetGenericTypeArguments(dictType)[1]); + return Activator.CreateInstance(wrapperType, value); + } + } + + return value; + } + + /// + /// Holds a dictionary that maps a cache of interface types for related concrete types. + /// The lookup is slow the first time for each type because it has to enumerate all interface + /// on the object type, but made fast by the cache. + /// + /// The map is: + /// (object type, generic interface type) => constructed generic type + /// + private static readonly Dictionary, Type> interfaceLookupCache = + new Dictionary, Type>(); + private static Type GetInterfaceType(Type objType, Type genericInterfaceType) + { + // Side note: It so sucks to have to do this. What a piece of crap bit of code + // Unfortunately, .NET doesn't provide any of the right hooks to do this for you + // *sigh* + if (ReflectionHelpers.IsConstructedGenericType(genericInterfaceType)) + { + genericInterfaceType = genericInterfaceType.GetGenericTypeDefinition(); + } + var cacheKey = new Tuple(objType, genericInterfaceType); + if (interfaceLookupCache.ContainsKey(cacheKey)) + { + return interfaceLookupCache[cacheKey]; + } + foreach (var type in ReflectionHelpers.GetInterfaces(objType)) + { + if (ReflectionHelpers.IsConstructedGenericType(type) && + type.GetGenericTypeDefinition() == genericInterfaceType) + { + return interfaceLookupCache[cacheKey] = type; + } + } + return null; + } + } +} diff --git a/Storage/Storage.PCL/Properties/AssemblyInfo.cs b/Storage/Storage.PCL/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..5ec291d --- /dev/null +++ b/Storage/Storage.PCL/Properties/AssemblyInfo.cs @@ -0,0 +1,26 @@ +using System.Reflection; +using System.Runtime.CompilerServices; + +// Information about this assembly is defined by the following attributes. +// Change them to the values specific to your project. + +[assembly: AssemblyTitle("Storage.PCL")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("")] +[assembly: AssemblyCopyright("${AuthorCopyright}")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}". +// The form "{Major}.{Minor}.*" will automatically update the build and revision, +// and "{Major}.{Minor}.{Build}.*" will update just the revision. + +[assembly: AssemblyVersion("1.0.*")] + +// The following attributes are used to specify the signing key for the assembly, +// if desired. See the Mono documentation for more information about signing. + +//[assembly: AssemblyDelaySign(false)] +//[assembly: AssemblyKeyFile("")] diff --git a/Storage/Storage.PCL/Storage.PCL.csproj b/Storage/Storage.PCL/Storage.PCL.csproj new file mode 100644 index 0000000..99cc1ce --- /dev/null +++ b/Storage/Storage.PCL/Storage.PCL.csproj @@ -0,0 +1,363 @@ + + + + Debug + AnyCPU + {659D19F0-9A40-42C0-886C-555E64F16848} + {786C830F-07A1-408B-BD7F-6EE04809D6DB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + Library + Storage.PCL + Storage.PCL + v4.5 + Profile111 + 0.1.0 + + + true + full + false + bin\Debug + DEBUG; + prompt + 4 + + + true + bin\Release + prompt + 4 + + + + + Internal\IAVCorePlugins.cs + + + Internal\AVCorePlugins.cs + + + Internal\AppRouter\AppRouterController.cs + + + Internal\AppRouter\AppRouterState.cs + + + Internal\AppRouter\IAppRouterController.cs + + + Internal\Authentication\IAVAuthenticationProvider.cs + + + Internal\Cloud\Controller\IAVCloudCodeController.cs + + + Internal\Cloud\Controller\AVCloudCodeController.cs + + + Internal\Command\IAVCommandRunner.cs + + + Internal\Command\AVCommand.cs + + + Internal\Command\AVCommandRunner.cs + + + Internal\Config\Controller\IAVConfigController.cs + + + Internal\Config\Controller\IAVCurrentConfigController.cs + + + Internal\Config\Controller\AVConfigController.cs + + + Internal\Config\Controller\AVCurrentConfigController.cs + + + Internal\Encoding\NoObjectsEncoder.cs + + + Internal\Encoding\AVDecoder.cs + + + Internal\Encoding\AVEncoder.cs + + + Internal\Encoding\AVObjectCoder.cs + + + Internal\Encoding\PointerOrLocalIdEncoder.cs + + + Internal\File\Controller\AWSS3FileController.cs + + + Internal\File\Controller\IAVFileController.cs + + + Internal\File\Controller\AVFileController.cs + + + Internal\File\Controller\QCloudCosFileController.cs + + + Internal\File\Controller\QiniuFileController.cs + + + Internal\File\Cryptography\MD5\MD5.cs + + + Internal\File\Cryptography\SHA1\SHA1CryptoServiceProvider.cs + + + Internal\File\State\FileState.cs + + + Internal\HttpClient\HttpRequest.cs + + + Internal\HttpClient\IHttpClient.cs + + + Internal\HttpClient\Portable\HttpClient.Portable.cs + + + Internal\InstallationId\Controller\IInstallationIdController.cs + + + Internal\InstallationId\Controller\InstallationIdController.cs + + + Internal\Object\Controller\IAVObjectController.cs + + + Internal\Object\Controller\IAVObjectCurrentController.cs + + + Internal\Object\Controller\AVObjectController.cs + + + Internal\Object\State\IObjectState.cs + + + Internal\Object\State\MutableObjectState.cs + + + Internal\Object\Subclassing\IObjectSubclassingController.cs + + + Internal\Object\Subclassing\ObjectSubclassInfo.cs + + + Internal\Object\Subclassing\ObjectSubclassingController.cs + + + Internal\Operation\IAVFieldOperation.cs + + + Internal\Operation\AVAddOperation.cs + + + Internal\Operation\AVAddUniqueOperation.cs + + + Internal\Operation\AVDeleteOperation.cs + + + Internal\Operation\AVFieldOperations.cs + + + Internal\Operation\AVIncrementOperation.cs + + + Internal\Operation\AVRelationOperation.cs + + + Internal\Operation\AVRemoveOperation.cs + + + Internal\Operation\AVSetOperation.cs + + + Internal\Query\Controller\IAVQueryController.cs + + + Internal\Query\Controller\AVQueryController.cs + + + Internal\Session\Controller\IAVSessionController.cs + + + Internal\Session\Controller\AVSessionController.cs + + + Internal\Storage\IStorageController.cs + + + Internal\Storage\Portable\StorageController.cs + + + Internal\User\Controller\IAVCurrentUserController.cs + + + Internal\User\Controller\IAVUserController.cs + + + Internal\User\Controller\AVCurrentUserController.cs + + + Internal\User\Controller\AVUserController.cs + + + Internal\Utilities\FlexibleDictionaryWrapper.cs + + + Internal\Utilities\FlexibleListWrapper.cs + + + Internal\Utilities\IJsonConvertible.cs + + + Internal\Utilities\IdentityEqualityComparer.cs + + + Internal\Utilities\InternalExtensions.cs + + + Internal\Utilities\Json.cs + + + Internal\Utilities\LockSet.cs + + + Internal\Utilities\AVConfigExtensions.cs + + + Internal\Utilities\AVFileExtensions.cs + + + Internal\Utilities\AVObjectExtensions.cs + + + Internal\Utilities\AVQueryExtensions.cs + + + Internal\Utilities\AVRelationExtensions.cs + + + Internal\Utilities\AVSessionExtensions.cs + + + Internal\Utilities\AVUserExtensions.cs + + + Internal\Utilities\ReflectionHelpers.cs + + + Internal\Utilities\SynchronizedEventHandler.cs + + + Internal\Utilities\TaskQueue.cs + + + Internal\Utilities\XamarinAttributes.cs + + + Public\AVACL.cs + + + Public\AVClassNameAttribute.cs + + + Public\AVClient.cs + + + Public\AVCloud.cs + + + Public\AVConfig.cs + + + Public\AVDownloadProgressEventArgs.cs + + + Public\AVException.cs + + + Public\AVExtensions.cs + + + Public\AVFieldNameAttribute.cs + + + Public\AVFile.cs + + + Public\AVGeoDistance.cs + + + Public\AVGeoPoint.cs + + + Public\AVObject.cs + + + Public\AVQuery.cs + + + Public\AVQueryExtensions.cs + + + Public\AVRelation.cs + + + Public\AVRole.cs + + + Public\AVSession.cs + + + Public\AVStatus.cs + + + Public\AVUploadProgressEventArgs.cs + + + Public\AVUser.cs + + + Public\AVUserAuthDataLogInOption.cs + + + Public\IAVQuery.cs + + + Public\LeaderBoard\AVLeaderboard.cs + + + Public\LeaderBoard\AVLeaderboardArchive.cs + + + Public\LeaderBoard\AVRanking.cs + + + Public\LeaderBoard\AVStatistic.cs + + + Public\Utilities\Conversion.cs + + + + + + + + ..\..\packages\PCLStorage.1.0.2\lib\portable-net45+wp8+wpa81+win8+monoandroid+monotouch+Xamarin.iOS+Xamarin.Mac\PCLStorage.Abstractions.dll + + + ..\..\packages\PCLStorage.1.0.2\lib\portable-net45+wp8+wpa81+win8+monoandroid+monotouch+Xamarin.iOS+Xamarin.Mac\PCLStorage.dll + + + + \ No newline at end of file diff --git a/Storage/Storage.PCL/packages.config b/Storage/Storage.PCL/packages.config new file mode 100644 index 0000000..5306567 --- /dev/null +++ b/Storage/Storage.PCL/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/Storage/Storage.Test/Storage.Test.csproj b/Storage/Storage.Test/Storage.Test.csproj new file mode 100644 index 0000000..fcae868 --- /dev/null +++ b/Storage/Storage.Test/Storage.Test.csproj @@ -0,0 +1,48 @@ + + + + + Debug + AnyCPU + {04DA35BB-6473-4D99-8A33-F499D40047E6} + Library + Storage.Test + Storage.Test + v4.7 + 0.1.0 + + + true + full + false + bin\Debug + DEBUG; + prompt + 4 + + + true + bin\Release + prompt + 4 + + + + + ..\..\packages\NUnit.3.12.0\lib\net45\nunit.framework.dll + + + + + + + + + + + {659D19F0-9A40-42C0-886C-555E64F16848} + Storage.PCL + + + + \ No newline at end of file diff --git a/Storage/Storage.Test/Test.cs b/Storage/Storage.Test/Test.cs new file mode 100644 index 0000000..e2edb6b --- /dev/null +++ b/Storage/Storage.Test/Test.cs @@ -0,0 +1,21 @@ +using NUnit.Framework; +using System; +using System.Reflection; +using System.Diagnostics; +using LeanCloud; + +namespace Storage.Test { + [TestFixture()] + public class Test { + [Test()] + public void TestCase() { + Assembly assembly = Assembly.GetEntryAssembly(); + var attr = assembly.GetCustomAttribute(); + Console.WriteLine(attr.InformationalVersion); + + FileVersionInfo versionInfo = FileVersionInfo.GetVersionInfo(assembly.Location); + String version = versionInfo.FileVersion; + Console.WriteLine(version); + } + } +} diff --git a/Storage/Storage.Test/packages.config b/Storage/Storage.Test/packages.config new file mode 100644 index 0000000..ded08cf --- /dev/null +++ b/Storage/Storage.Test/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/Storage/Storage.Unity/Properties/AssemblyInfo.cs b/Storage/Storage.Unity/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..78f7194 --- /dev/null +++ b/Storage/Storage.Unity/Properties/AssemblyInfo.cs @@ -0,0 +1,26 @@ +using System.Reflection; +using System.Runtime.CompilerServices; + +// Information about this assembly is defined by the following attributes. +// Change them to the values specific to your project. + +[assembly: AssemblyTitle("Storage.Unity")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("")] +[assembly: AssemblyCopyright("${AuthorCopyright}")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}". +// The form "{Major}.{Minor}.*" will automatically update the build and revision, +// and "{Major}.{Minor}.{Build}.*" will update just the revision. + +[assembly: AssemblyVersion("1.0.*")] + +// The following attributes are used to specify the signing key for the assembly, +// if desired. See the Mono documentation for more information about signing. + +//[assembly: AssemblyDelaySign(false)] +//[assembly: AssemblyKeyFile("")] diff --git a/Storage/Storage.Unity/Storage.Unity.csproj b/Storage/Storage.Unity/Storage.Unity.csproj new file mode 100644 index 0000000..259956b --- /dev/null +++ b/Storage/Storage.Unity/Storage.Unity.csproj @@ -0,0 +1,368 @@ + + + + Debug + AnyCPU + {A0D50BCB-E50E-4AAE-8E7D-24BF5AE33DAC} + Library + Storage.Unity + Storage.Unity + v4.5 + 0.1.0 + + + true + full + false + bin\Debug + DEBUG;UNITY; + prompt + 4 + false + + + true + bin\Release + UNITY; + prompt + 4 + false + + + + + ..\..\Libs\UnityEngine.dll + + + + + + Internal\IAVCorePlugins.cs + + + Internal\AVCorePlugins.cs + + + Internal\AppRouter\AppRouterController.cs + + + Internal\AppRouter\AppRouterState.cs + + + Internal\AppRouter\IAppRouterController.cs + + + Internal\Cloud\Controller\IAVCloudCodeController.cs + + + Internal\Cloud\Controller\AVCloudCodeController.cs + + + Internal\Command\IAVCommandRunner.cs + + + Internal\Command\AVCommand.cs + + + Internal\Command\AVCommandRunner.cs + + + Internal\Config\Controller\IAVConfigController.cs + + + Internal\Config\Controller\IAVCurrentConfigController.cs + + + Internal\Config\Controller\AVConfigController.cs + + + Internal\Config\Controller\AVCurrentConfigController.cs + + + Internal\Dispatcher\Unity\UnityDispatcher.cs + + + Internal\Encoding\NoObjectsEncoder.cs + + + Internal\Encoding\AVDecoder.cs + + + Internal\Encoding\AVEncoder.cs + + + Internal\Encoding\AVObjectCoder.cs + + + Internal\Encoding\PointerOrLocalIdEncoder.cs + + + Internal\File\Controller\AWSS3FileController.cs + + + Internal\File\Controller\IAVFileController.cs + + + Internal\File\Controller\AVFileController.cs + + + Internal\File\Controller\QCloudCosFileController.cs + + + Internal\File\Controller\QiniuFileController.cs + + + Internal\File\Cryptography\MD5\MD5.cs + + + Internal\File\Cryptography\SHA1\SHA1CryptoServiceProvider.cs + + + Internal\File\State\FileState.cs + + + Internal\HttpClient\HttpRequest.cs + + + Internal\HttpClient\IHttpClient.cs + + + Internal\HttpClient\Unity\HttpClient.Unity.cs + + + Internal\InstallationId\Controller\IInstallationIdController.cs + + + Internal\InstallationId\Controller\InstallationIdController.cs + + + Internal\Object\Controller\IAVObjectController.cs + + + Internal\Object\Controller\IAVObjectCurrentController.cs + + + Internal\Object\Controller\AVObjectController.cs + + + Internal\Object\State\IObjectState.cs + + + Internal\Object\State\MutableObjectState.cs + + + Internal\Object\Subclassing\IObjectSubclassingController.cs + + + Internal\Object\Subclassing\ObjectSubclassInfo.cs + + + Internal\Object\Subclassing\ObjectSubclassingController.cs + + + Internal\Operation\IAVFieldOperation.cs + + + Internal\Operation\AVAddOperation.cs + + + Internal\Operation\AVAddUniqueOperation.cs + + + Internal\Operation\AVDeleteOperation.cs + + + Internal\Operation\AVFieldOperations.cs + + + Internal\Operation\AVIncrementOperation.cs + + + Internal\Operation\AVRelationOperation.cs + + + Internal\Operation\AVRemoveOperation.cs + + + Internal\Operation\AVSetOperation.cs + + + Internal\Query\Controller\IAVQueryController.cs + + + Internal\Query\Controller\AVQueryController.cs + + + Internal\Session\Controller\IAVSessionController.cs + + + Internal\Session\Controller\AVSessionController.cs + + + Internal\Storage\IStorageController.cs + + + Internal\Storage\Unity\StorageController.cs + + + Internal\User\Controller\IAVCurrentUserController.cs + + + Internal\User\Controller\IAVUserController.cs + + + Internal\User\Controller\AVCurrentUserController.cs + + + Internal\User\Controller\AVUserController.cs + + + Internal\Utilities\FlexibleDictionaryWrapper.cs + + + Internal\Utilities\FlexibleListWrapper.cs + + + Internal\Utilities\IJsonConvertible.cs + + + Internal\Utilities\IdentityEqualityComparer.cs + + + Internal\Utilities\InternalExtensions.cs + + + Internal\Utilities\Json.cs + + + Internal\Utilities\LockSet.cs + + + Internal\Utilities\AVConfigExtensions.cs + + + Internal\Utilities\AVFileExtensions.cs + + + Internal\Utilities\AVObjectExtensions.cs + + + Internal\Utilities\AVQueryExtensions.cs + + + Internal\Utilities\AVRelationExtensions.cs + + + Internal\Utilities\AVSessionExtensions.cs + + + Internal\Utilities\AVUserExtensions.cs + + + Internal\Utilities\ReflectionHelpers.cs + + + Internal\Utilities\SynchronizedEventHandler.cs + + + Internal\Utilities\TaskQueue.cs + + + Internal\Utilities\XamarinAttributes.cs + + + Public\AVACL.cs + + + Public\AVClassNameAttribute.cs + + + Public\AVClient.cs + + + Public\AVCloud.cs + + + Public\AVConfig.cs + + + Public\AVDownloadProgressEventArgs.cs + + + Public\AVException.cs + + + Public\AVExtensions.cs + + + Public\AVFieldNameAttribute.cs + + + Public\AVFile.cs + + + Public\AVGeoDistance.cs + + + Public\AVGeoPoint.cs + + + Public\AVObject.cs + + + Public\AVQuery.cs + + + Public\AVQueryExtensions.cs + + + Public\AVRelation.cs + + + Public\AVRole.cs + + + Public\AVSession.cs + + + Public\AVStatus.cs + + + Public\AVUploadProgressEventArgs.cs + + + Public\AVUser.cs + + + Public\AVUserAuthDataLogInOption.cs + + + Public\IAVQuery.cs + + + Public\LeaderBoard\AVLeaderboard.cs + + + Public\LeaderBoard\AVLeaderboardArchive.cs + + + Public\LeaderBoard\AVRanking.cs + + + Public\LeaderBoard\AVStatistic.cs + + + Public\Unity\AVInitializeBehaviour.cs + + + Public\Utilities\Conversion.cs + + + Internal\Authentication\IAVAuthenticationProvider.cs + + + + + + + \ No newline at end of file diff --git a/csharp-sdk.sln b/csharp-sdk.sln new file mode 100644 index 0000000..26bcd87 --- /dev/null +++ b/csharp-sdk.sln @@ -0,0 +1,87 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Storage", "Storage", "{CD6B6669-1A56-437A-932E-BCE7F5D4CD18}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "RTM", "RTM", "{64D8F9A1-BA44-459C-817C-788B4EBC0B9F}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "LiveQuery", "LiveQuery", "{5B895B7A-1F6E-40A5-8081-43B334D2C076}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "LeanEngine", "LeanEngine", "{CDC4E11B-592E-48C3-9105-CD49435424AA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Storage.PCL", "Storage\Storage.PCL\Storage.PCL.csproj", "{659D19F0-9A40-42C0-886C-555E64F16848}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Storage.Unity", "Storage\Storage.Unity\Storage.Unity.csproj", "{A0D50BCB-E50E-4AAE-8E7D-24BF5AE33DAC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Storage.Test", "Storage\Storage.Test\Storage.Test.csproj", "{04DA35BB-6473-4D99-8A33-F499D40047E6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RTM.PCL", "RTM\RTM.PCL\RTM.PCL.csproj", "{92B2B40E-A3CD-4672-AC84-2E894E1A6CE5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RTM.Unity", "RTM\RTM.Unity\RTM.Unity.csproj", "{1E608FCD-9039-4FF7-8EE7-BA8B00E15D1C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RTM.Test", "RTM\RTM.Test\RTM.Test.csproj", "{A1BBD0B5-41C6-4579-B9A3-5EF778BE7F95}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LiveQuery.PCL", "LiveQuery\LiveQuery.PCL\LiveQuery.PCL.csproj", "{EA1C601E-D853-41F7-B9EB-276CBF7D1FA5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LiveQuery.Unity", "LiveQuery\LiveQuery.Unity\LiveQuery.Unity.csproj", "{3251B4D8-D11A-4D90-8626-27FEE266B066}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LiveQuery.Test", "LiveQuery\LiveQuery.Test\LiveQuery.Test.csproj", "{F907012C-74DF-4575-AFE6-E8DAACC26D24}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {659D19F0-9A40-42C0-886C-555E64F16848}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {659D19F0-9A40-42C0-886C-555E64F16848}.Debug|Any CPU.Build.0 = Debug|Any CPU + {659D19F0-9A40-42C0-886C-555E64F16848}.Release|Any CPU.ActiveCfg = Release|Any CPU + {659D19F0-9A40-42C0-886C-555E64F16848}.Release|Any CPU.Build.0 = Release|Any CPU + {A0D50BCB-E50E-4AAE-8E7D-24BF5AE33DAC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A0D50BCB-E50E-4AAE-8E7D-24BF5AE33DAC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A0D50BCB-E50E-4AAE-8E7D-24BF5AE33DAC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A0D50BCB-E50E-4AAE-8E7D-24BF5AE33DAC}.Release|Any CPU.Build.0 = Release|Any CPU + {04DA35BB-6473-4D99-8A33-F499D40047E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {04DA35BB-6473-4D99-8A33-F499D40047E6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {04DA35BB-6473-4D99-8A33-F499D40047E6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {04DA35BB-6473-4D99-8A33-F499D40047E6}.Release|Any CPU.Build.0 = Release|Any CPU + {92B2B40E-A3CD-4672-AC84-2E894E1A6CE5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {92B2B40E-A3CD-4672-AC84-2E894E1A6CE5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {92B2B40E-A3CD-4672-AC84-2E894E1A6CE5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {92B2B40E-A3CD-4672-AC84-2E894E1A6CE5}.Release|Any CPU.Build.0 = Release|Any CPU + {1E608FCD-9039-4FF7-8EE7-BA8B00E15D1C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1E608FCD-9039-4FF7-8EE7-BA8B00E15D1C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1E608FCD-9039-4FF7-8EE7-BA8B00E15D1C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1E608FCD-9039-4FF7-8EE7-BA8B00E15D1C}.Release|Any CPU.Build.0 = Release|Any CPU + {A1BBD0B5-41C6-4579-B9A3-5EF778BE7F95}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A1BBD0B5-41C6-4579-B9A3-5EF778BE7F95}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1BBD0B5-41C6-4579-B9A3-5EF778BE7F95}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A1BBD0B5-41C6-4579-B9A3-5EF778BE7F95}.Release|Any CPU.Build.0 = Release|Any CPU + {EA1C601E-D853-41F7-B9EB-276CBF7D1FA5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EA1C601E-D853-41F7-B9EB-276CBF7D1FA5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EA1C601E-D853-41F7-B9EB-276CBF7D1FA5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EA1C601E-D853-41F7-B9EB-276CBF7D1FA5}.Release|Any CPU.Build.0 = Release|Any CPU + {3251B4D8-D11A-4D90-8626-27FEE266B066}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3251B4D8-D11A-4D90-8626-27FEE266B066}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3251B4D8-D11A-4D90-8626-27FEE266B066}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3251B4D8-D11A-4D90-8626-27FEE266B066}.Release|Any CPU.Build.0 = Release|Any CPU + {F907012C-74DF-4575-AFE6-E8DAACC26D24}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F907012C-74DF-4575-AFE6-E8DAACC26D24}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F907012C-74DF-4575-AFE6-E8DAACC26D24}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F907012C-74DF-4575-AFE6-E8DAACC26D24}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {659D19F0-9A40-42C0-886C-555E64F16848} = {CD6B6669-1A56-437A-932E-BCE7F5D4CD18} + {A0D50BCB-E50E-4AAE-8E7D-24BF5AE33DAC} = {CD6B6669-1A56-437A-932E-BCE7F5D4CD18} + {04DA35BB-6473-4D99-8A33-F499D40047E6} = {CD6B6669-1A56-437A-932E-BCE7F5D4CD18} + {92B2B40E-A3CD-4672-AC84-2E894E1A6CE5} = {64D8F9A1-BA44-459C-817C-788B4EBC0B9F} + {1E608FCD-9039-4FF7-8EE7-BA8B00E15D1C} = {64D8F9A1-BA44-459C-817C-788B4EBC0B9F} + {A1BBD0B5-41C6-4579-B9A3-5EF778BE7F95} = {64D8F9A1-BA44-459C-817C-788B4EBC0B9F} + {EA1C601E-D853-41F7-B9EB-276CBF7D1FA5} = {5B895B7A-1F6E-40A5-8081-43B334D2C076} + {3251B4D8-D11A-4D90-8626-27FEE266B066} = {5B895B7A-1F6E-40A5-8081-43B334D2C076} + {F907012C-74DF-4575-AFE6-E8DAACC26D24} = {5B895B7A-1F6E-40A5-8081-43B334D2C076} + EndGlobalSection + GlobalSection(MonoDevelopProperties) = preSolution + version = 0.1.0 + EndGlobalSection +EndGlobal