diff --git a/actor-sdk/sdk-api/actor.json b/actor-sdk/sdk-api/actor.json index b27bd3cb55..b1f2e2165f 100644 --- a/actor-sdk/sdk-api/actor.json +++ b/actor-sdk/sdk-api/actor.json @@ -4738,6 +4738,33 @@ "attributes": [] } }, + { + "type": "struct", + "content": { + "name": "ServiceTimerChanged", + "doc": [ + "Service Message about timer changed", + { + "type": "reference", + "argument": "timerMs", + "category": "full", + "description": " Timer in milliseconds" + } + ], + "trait": { + "name": "ServiceEx", + "key": 23 + }, + "expandable": "true", + "attributes": [ + { + "type": "int32", + "id": 1, + "name": "timerMs" + } + ] + } + }, { "type": "struct", "content": { @@ -4785,6 +4812,12 @@ "argument": "ext", "category": "compact", "description": " Extension" + }, + { + "type": "reference", + "argument": "encryptionInfo", + "category": "full", + "description": " Optional information for encrypted documents" } ], "trait": { @@ -4839,6 +4872,61 @@ }, "id": 8, "name": "ext" + }, + { + "type": { + "type": "opt", + "childType": { + "type": "struct", + "childType": "DocumentEncryptionInfo" + } + }, + "id": 9, + "name": "encryptionInfo" + } + ] + } + }, + { + "type": "struct", + "content": { + "name": "DocumentEncryptionInfo", + "doc": [ + "Document encryption key", + { + "type": "reference", + "argument": "realFileSize", + "category": "full", + "description": " File Size" + }, + { + "type": "reference", + "argument": "keyAlg", + "category": "full", + "description": " Key Algorithm" + }, + { + "type": "reference", + "argument": "key", + "category": "full", + "description": " Key material" + } + ], + "attributes": [ + { + "type": "int32", + "id": 1, + "name": "realFileSize" + }, + { + "type": "string", + "id": 2, + "name": "keyAlg" + }, + { + "type": "bytes", + "id": 3, + "name": "key" } ] } @@ -5239,36 +5327,6 @@ ] } }, - { - "type": "struct", - "content": { - "name": "EncryptedMessage", - "doc": [ - "Encrypted Message", - { - "type": "reference", - "argument": "box", - "category": "full", - "description": " Encrypted box" - } - ], - "trait": { - "name": "Message", - "key": 8 - }, - "expandable": "true", - "attributes": [ - { - "type": { - "type": "struct", - "childType": "EncryptedBox" - }, - "id": 1, - "name": "box" - } - ] - } - }, { "type": "struct", "content": { @@ -19819,6 +19877,432 @@ } ] } + }, + { + "type": "comment", + "content": "Encrypted Content Container" + }, + { + "type": "struct", + "content": { + "name": "EncryptedData", + "doc": [ + "Encrypted Content", + { + "type": "reference", + "argument": "version", + "category": "full", + "description": " Version of data" + }, + { + "type": "reference", + "argument": "data", + "category": "full", + "description": " Data" + } + ], + "attributes": [ + { + "type": "int32", + "id": 1, + "name": "version" + }, + { + "type": "bytes", + "id": 2, + "name": "data" + } + ] + } + }, + { + "type": "struct", + "content": { + "name": "EncryptedGroup", + "doc": [ + "Encrypted Group information", + { + "type": "reference", + "argument": "groupId", + "category": "full", + "description": " Group Random Id" + }, + { + "type": "reference", + "argument": "title", + "category": "full", + "description": " Group Title" + }, + { + "type": "reference", + "argument": "members", + "category": "compact", + "description": " Group Members" + } + ], + "attributes": [ + { + "type": "int64", + "id": 1, + "name": "groupId" + }, + { + "type": "string", + "id": 2, + "name": "title" + }, + { + "type": { + "type": "list", + "childType": "int32" + }, + "id": 3, + "name": "members" + } + ] + } + }, + { + "type": "trait", + "content": { + "isContainer": "true", + "name": "EncryptedContent", + "attributes": [] + } + }, + { + "type": "struct", + "content": { + "name": "EncryptedMessageContent", + "doc": [ + "New incoming encrypted message", + { + "type": "reference", + "argument": "receiverId", + "category": "full", + "description": " Receiver User Id" + }, + { + "type": "reference", + "argument": "rid", + "category": "full", + "description": " Random id of message" + }, + { + "type": "reference", + "argument": "message", + "category": "full", + "description": " Content of message" + }, + { + "type": "reference", + "argument": "timerMs", + "category": "full", + "description": " Optional self-destruct timer" + } + ], + "trait": { + "name": "EncryptedContent", + "key": 1 + }, + "attributes": [ + { + "type": { + "type": "alias", + "childType": "userId" + }, + "id": 1, + "name": "receiverId" + }, + { + "type": { + "type": "alias", + "childType": "randomId" + }, + "id": 2, + "name": "rid" + }, + { + "type": { + "type": "trait", + "childType": "Message" + }, + "id": 3, + "name": "message" + }, + { + "type": { + "type": "opt", + "childType": "int32" + }, + "id": 4, + "name": "timerMs" + } + ] + } + }, + { + "type": "struct", + "content": { + "name": "EncryptedEditContent", + "doc": [ + "Encrypted message edit", + { + "type": "reference", + "argument": "receiverId", + "category": "full", + "description": " Receiver User Id" + }, + { + "type": "reference", + "argument": "rid", + "category": "full", + "description": " Random id of message" + }, + { + "type": "reference", + "argument": "message", + "category": "full", + "description": " Updated content of message" + } + ], + "trait": { + "name": "EncryptedContent", + "key": 2 + }, + "attributes": [ + { + "type": { + "type": "alias", + "childType": "userId" + }, + "id": 1, + "name": "receiverId" + }, + { + "type": { + "type": "alias", + "childType": "randomId" + }, + "id": 2, + "name": "rid" + }, + { + "type": { + "type": "trait", + "childType": "Message" + }, + "id": 3, + "name": "message" + } + ] + } + }, + { + "type": "struct", + "content": { + "name": "EncryptedDeleteContent", + "doc": [ + "Encrypted message delete", + { + "type": "reference", + "argument": "receiverId", + "category": "full", + "description": " Receiver User Id" + }, + { + "type": "reference", + "argument": "rid", + "category": "full", + "description": " Random id of message" + } + ], + "trait": { + "name": "EncryptedContent", + "key": 3 + }, + "attributes": [ + { + "type": { + "type": "alias", + "childType": "userId" + }, + "id": 1, + "name": "receiverId" + }, + { + "type": { + "type": "list", + "childType": { + "type": "alias", + "childType": "randomId" + } + }, + "id": 2, + "name": "rid" + } + ] + } + }, + { + "type": "struct", + "content": { + "name": "EncryptedReceived", + "doc": [ + "Encrypted message receive notification", + { + "type": "reference", + "argument": "receiverId", + "category": "full", + "description": " Receiver User Id" + }, + { + "type": "reference", + "argument": "receiveDate", + "category": "full", + "description": " Receive message sort date" + } + ], + "trait": { + "name": "EncryptedContent", + "key": 4 + }, + "attributes": [ + { + "type": { + "type": "alias", + "childType": "userId" + }, + "id": 1, + "name": "receiverId" + }, + { + "type": "int64", + "id": 2, + "name": "receiveDate" + } + ] + } + }, + { + "type": "struct", + "content": { + "name": "EncryptedRead", + "doc": [ + "Encrypted message read notification", + { + "type": "reference", + "argument": "receiverId", + "category": "full", + "description": " Receiver User Id" + }, + { + "type": "reference", + "argument": "readDate", + "category": "full", + "description": " Read message sort date" + } + ], + "trait": { + "name": "EncryptedContent", + "key": 5 + }, + "attributes": [ + { + "type": { + "type": "alias", + "childType": "userId" + }, + "id": 1, + "name": "receiverId" + }, + { + "type": "int64", + "id": 2, + "name": "readDate" + } + ] + } + }, + { + "type": "struct", + "content": { + "name": "EncryptedDeleteAll", + "doc": [ + "Encrypted message about clearing chat", + { + "type": "reference", + "argument": "receiverId", + "category": "full", + "description": " Receiver User Id" + } + ], + "trait": { + "name": "EncryptedContent", + "key": 6 + }, + "attributes": [ + { + "type": { + "type": "alias", + "childType": "userId" + }, + "id": 1, + "name": "receiverId" + } + ] + } + }, + { + "type": "struct", + "content": { + "name": "EncryptedChatTimerSet", + "doc": [ + "Encrypted message about timer setting", + { + "type": "reference", + "argument": "receiverId", + "category": "full", + "description": " Receiver User Id" + }, + { + "type": "reference", + "argument": "timerMs", + "category": "full", + "description": " Timer in MS" + } + ], + "trait": { + "name": "EncryptedContent", + "key": 8 + }, + "attributes": [ + { + "type": { + "type": "alias", + "childType": "userId" + }, + "id": 1, + "name": "receiverId" + }, + { + "type": { + "type": "alias", + "childType": "randomId" + }, + "id": 3, + "name": "rid" + }, + { + "type": { + "type": "opt", + "childType": "int32" + }, + "id": 2, + "name": "timerMs" + } + ] + } } ] }, diff --git a/actor-sdk/sdk-api/api-language/languages/im.actor.apiLanguage/languageModels/editor.mps b/actor-sdk/sdk-api/api-language/languages/im.actor.apiLanguage/languageModels/editor.mps index 0a6cb28361..23be5af374 100644 --- a/actor-sdk/sdk-api/api-language/languages/im.actor.apiLanguage/languageModels/editor.mps +++ b/actor-sdk/sdk-api/api-language/languages/im.actor.apiLanguage/languageModels/editor.mps @@ -1322,12 +1322,6 @@ - - - - - - @@ -1389,6 +1383,9 @@ + + + diff --git a/actor-sdk/sdk-api/api-language/solutions/im.actor.api/models/im/actor/api/scheme.mps b/actor-sdk/sdk-api/api-language/solutions/im.actor.api/models/im/actor/api/scheme.mps index dc46e3760b..f57cf45406 100644 --- a/actor-sdk/sdk-api/api-language/solutions/im.actor.api/models/im/actor/api/scheme.mps +++ b/actor-sdk/sdk-api/api-language/solutions/im.actor.api/models/im/actor/api/scheme.mps @@ -4341,6 +4341,28 @@ + + + + + + + + + + + + + + + + + + + + + + @@ -4384,6 +4406,11 @@ + + + + + @@ -4427,10 +4454,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -4762,30 +4834,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - @@ -16844,6 +16892,353 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/actor-sdk/sdk-core-android/android-app/src/main/java/im/actor/Application.java b/actor-sdk/sdk-core-android/android-app/src/main/java/im/actor/Application.java index f20f359837..843a9ccf7a 100644 --- a/actor-sdk/sdk-core-android/android-app/src/main/java/im/actor/Application.java +++ b/actor-sdk/sdk-core-android/android-app/src/main/java/im/actor/Application.java @@ -59,7 +59,7 @@ public void onConfigureActorSDK() { ActorStyle style = ActorSDK.sharedActor().style; style.setDialogsActiveTextColor(0xff5882ac); - style.setShowAvatarPrivateInTitle(false); + // style.setShowAvatarPrivateInTitle(false); ActorSDK.sharedActor().setFastShareEnabled(true); diff --git a/actor-sdk/sdk-core-android/android-google-maps/build.gradle b/actor-sdk/sdk-core-android/android-google-maps/build.gradle index 88ae171a0a..2bf27e6c08 100644 --- a/actor-sdk/sdk-core-android/android-google-maps/build.gradle +++ b/actor-sdk/sdk-core-android/android-google-maps/build.gradle @@ -18,7 +18,7 @@ apply plugin: 'me.tatarka.retrolambda' group = 'im.actor' version = '0.0.1' -def baseVersion = "3.0" +def baseVersion = "4.0" android { compileSdkVersion 24 diff --git a/actor-sdk/sdk-core-android/android-google-push/build.gradle b/actor-sdk/sdk-core-android/android-google-push/build.gradle index 499980ac4a..9e70045e49 100644 --- a/actor-sdk/sdk-core-android/android-google-push/build.gradle +++ b/actor-sdk/sdk-core-android/android-google-push/build.gradle @@ -16,7 +16,7 @@ apply plugin: 'me.tatarka.retrolambda' group = 'im.actor' version = '0.0.1' -def baseVersion = "3.0" +def baseVersion = "4.0" android { compileSdkVersion 24 diff --git a/actor-sdk/sdk-core-android/android-sdk/build.gradle b/actor-sdk/sdk-core-android/android-sdk/build.gradle index 787550856c..b9ba8e8877 100644 --- a/actor-sdk/sdk-core-android/android-sdk/build.gradle +++ b/actor-sdk/sdk-core-android/android-sdk/build.gradle @@ -16,7 +16,7 @@ apply plugin: 'me.tatarka.retrolambda' group = 'im.actor' version = '0.0.1' -def baseVersion = "3.0" +def baseVersion = "4.0" android { diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/ActorStyle.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/ActorStyle.java index b774c5cc13..8267ea2be5 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/ActorStyle.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/ActorStyle.java @@ -226,6 +226,25 @@ public void setVerifiedColor(int verifiedColor) { this.verifiedColor = verifiedColor; } + private int secretChatToolbar = 0xff4CAF50; + private int secretChatStatusbar = 0xff388E3C; + + public int getSecretChatToolbar() { + return secretChatToolbar; + } + + public void setSecretChatToolbar(int secretChatToolbar) { + this.secretChatToolbar = secretChatToolbar; + } + + public int getSecretChatStatusbar() { + return secretChatStatusbar; + } + + public void setSecretChatStatusbar(int secretChatStatusbar) { + this.secretChatStatusbar = secretChatStatusbar; + } + // // List Styles // diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/Intents.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/Intents.java index 00453de81e..c81f8ae420 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/Intents.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/Intents.java @@ -122,6 +122,10 @@ public static Intent openPrivateDialog(int uid, boolean compose, Context context return openDialog(Peer.user(uid), compose, context); } + public static Intent openPrivateSecretDialog(int uid, boolean compose, Context context) { + return openDialog(Peer.secret(uid), compose, context); + } + public static Intent openGroupDialog(int chatId, boolean compose, Context context) { return openDialog(Peer.group(chatId), compose, context); } diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/ChatActivity.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/ChatActivity.java index 5edd333c91..f0dfd08aac 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/ChatActivity.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/ChatActivity.java @@ -14,12 +14,16 @@ import android.util.TypedValue; import android.view.View; import android.view.ViewGroup; +import android.view.WindowManager; import android.widget.FrameLayout; import android.widget.LinearLayout; import android.widget.RelativeLayout; import im.actor.core.entity.Peer; import im.actor.sdk.ActorSDK; + +import im.actor.core.entity.Peer; +import im.actor.core.entity.PeerType; import im.actor.sdk.R; import im.actor.sdk.controllers.activity.BaseActivity; import im.actor.sdk.util.Screen; @@ -49,6 +53,13 @@ public void onCreate(Bundle saveInstance) { getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); + // Secure Window from screenshoting + Peer peer = Peer.fromUniqueId(getIntent().getExtras().getLong(EXTRA_CHAT_PEER)); + if (peer.getPeerType() == PeerType.PRIVATE_ENCRYPTED) { + getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, + WindowManager.LayoutParams.FLAG_SECURE); + } + // // Loading Layout // diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/MessagesDefaultFragment.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/MessagesDefaultFragment.java index 989cf29914..e5a63309b5 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/MessagesDefaultFragment.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/MessagesDefaultFragment.java @@ -15,6 +15,7 @@ import im.actor.core.entity.Message; import im.actor.core.entity.Peer; +import im.actor.core.entity.PeerType; import im.actor.core.entity.content.AbsContent; import im.actor.core.entity.content.TextContent; import im.actor.core.entity.content.UnsupportedContent; @@ -120,10 +121,18 @@ public boolean onPrepareActionMode(ActionMode actionMode, Menu menu) { } } - menu.findItem(R.id.copy).setVisible(isAllText); menu.findItem(R.id.quote).setVisible(isAllText); - menu.findItem(R.id.forward).setVisible(selected.length == 1 || isAllText); - menu.findItem(R.id.like).setVisible(selected.length == 1); + + if (peer.getPeerType() == PeerType.PRIVATE_ENCRYPTED) { + menu.findItem(R.id.copy).setVisible(false); + menu.findItem(R.id.forward).setVisible(false); + menu.findItem(R.id.like).setVisible(false); + } else { + menu.findItem(R.id.copy).setVisible(isAllText); + menu.findItem(R.id.forward).setVisible(selected.length == 1 || isAllText); + menu.findItem(R.id.like).setVisible(selected.length == 1); + } + return false; } diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/MessagesFragment.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/MessagesFragment.java index df180a6f63..3485533108 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/MessagesFragment.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/MessagesFragment.java @@ -17,6 +17,7 @@ import fr.castorflex.android.circularprogressbar.CircularProgressBar; import im.actor.core.entity.Message; import im.actor.core.entity.Peer; +import im.actor.core.entity.PeerType; import im.actor.core.viewmodel.ConversationVM; import im.actor.runtime.Log; import im.actor.sdk.ActorSDK; @@ -120,7 +121,8 @@ public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, } else { background = getResources().getDrawable(backgrounds[0]); } - ((ImageView) res.findViewById(R.id.chatBackgroundView)).setImageDrawable(background); + ImageView backgroundView = (ImageView) res.findViewById(R.id.chatBackgroundView); + backgroundView.setImageDrawable(background); // @@ -283,13 +285,17 @@ public void onResume() { } // Bind Progress - bind(conversationVM.getIsLoaded(), conversationVM.getIsEmpty(), (isLoaded, valueModel, isEmpty, valueModel2) -> { - if (isEmpty && !isLoaded) { - showView(progressView); - } else { - hideView(progressView); - } - }); + if (peer.getPeerType() != PeerType.PRIVATE_ENCRYPTED) { + bind(conversationVM.getIsLoaded(), conversationVM.getIsEmpty(), (isLoaded, valueModel, isEmpty, valueModel2) -> { + if (isEmpty && !isLoaded) { + showView(progressView); + } else { + hideView(progressView); + } + }); + } else { + hideView(progressView); + } } @Override diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/toolbar/ChatToolbarFragment.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/toolbar/ChatToolbarFragment.java index 0d9338537e..3294d2a931 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/toolbar/ChatToolbarFragment.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/toolbar/ChatToolbarFragment.java @@ -3,9 +3,12 @@ import android.Manifest; import android.app.Activity; import android.app.AlertDialog; +import android.content.DialogInterface; import android.content.pm.PackageManager; import android.graphics.Color; import android.graphics.PorterDuff; +import android.graphics.drawable.ColorDrawable; +import android.os.Build; import android.os.Bundle; import android.support.v4.app.ActivityCompat; import android.support.v4.content.ContextCompat; @@ -101,6 +104,16 @@ public void onConfigureActionBar(ActionBar actionBar) { actionBar.setDisplayShowHomeEnabled(false); actionBar.setDisplayShowCustomEnabled(true); + // Coloring Toolbar + if (peer.getPeerType() != PeerType.PRIVATE_ENCRYPTED) { + actionBar.setBackgroundDrawable(new ColorDrawable(style.getToolBarColor())); + } else { + actionBar.setBackgroundDrawable(new ColorDrawable(style.getSecretChatToolbar())); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + getActivity().getWindow().setStatusBarColor(style.getSecretChatStatusbar()); + } + } + // Loading Toolbar header views ActorStyle style = ActorSDK.sharedActor().style; barView = LayoutInflater.from(getActivity()).inflate(R.layout.bar_conversation, null); @@ -132,7 +145,7 @@ public void onConfigureActionBar(ActionBar actionBar) { barAvatar.init(Screen.dp(32), 18); barView.findViewById(R.id.titleContainer).setOnClickListener(v -> { - if (peer.getPeerType() == PeerType.PRIVATE) { + if (peer.getPeerType() == PeerType.PRIVATE || peer.getPeerType() == PeerType.PRIVATE_ENCRYPTED) { ActorSDKLauncher.startProfileActivity(getActivity(), peer.getPeerId()); } else if (peer.getPeerType() == PeerType.GROUP) { ActorSDK.sharedActor().startGroupInfoActivity(getActivity(), peer.getPeerId()); @@ -148,7 +161,7 @@ public void onResume() { // Performing all required Data Binding here - if (peer.getPeerType() == PeerType.PRIVATE) { + if (peer.getPeerType() == PeerType.PRIVATE || peer.getPeerType() == PeerType.PRIVATE_ENCRYPTED) { // Loading user UserVM user = users().get(peer.getPeerId()); @@ -171,7 +184,11 @@ public void onResume() { bind(barSubtitle, user); // Binding User typing to Toolbar - bindPrivateTyping(barTyping, barTypingContainer, barSubtitle, messenger().getTyping(user.getId())); + if (peer.getPeerType() != PeerType.PRIVATE_ENCRYPTED) { + bindPrivateTyping(barTyping, barTypingContainer, barSubtitle, messenger().getTyping(user.getId())); + } else { + bindPrivateTyping(barTyping, barTypingContainer, barSubtitle, messenger().getSecretTyping(user.getId())); + } // Refresh menu on contact state change bind(user.isContact(), (val, valueModel) -> { @@ -206,7 +223,14 @@ public void onResume() { bindGroupTyping(barTyping, barTypingContainer, barSubtitle, messenger().getGroupTyping(group.getId())); } } - + + // Show/Hide Avatar + if (!style.isShowAvatarInTitle() || + ((peer.getPeerType() == PeerType.PRIVATE || peer.getPeerType() == PeerType.PRIVATE_ENCRYPTED) + && !style.isShowAvatarPrivateInTitle())) { + barAvatar.setVisibility(View.GONE); + } + // Global Counter bind(messenger().getGlobalState().getGlobalCounter(), (val, valueModel) -> { if (val != null && val > 0) { @@ -224,33 +248,11 @@ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { // Inflating menu inflater.inflate(R.menu.chat_menu, menu); - - // Show menu for opening chat contact -// if (peer.getPeerType() == PeerType.PRIVATE) { -// menu.findItem(R.id.contact).setVisible(true); -// } else { -// menu.findItem(R.id.contact).setVisible(false); -// } - - // Show menus for leave group and group info view -// if (peer.getPeerType() == PeerType.GROUP) { -// GroupVM groupVM = groups().get(peer.getPeerId()); -// if (groupVM.isMember().get()) { -// menu.findItem(R.id.leaveGroup).setVisible(true); -// menu.findItem(R.id.groupInfo).setVisible(true); -// } else { -// menu.findItem(R.id.leaveGroup).setVisible(false); -// menu.findItem(R.id.groupInfo).setVisible(false); -// } -// if (groupVM.getGroupType() == GroupType.GROUP) { -// menu.findItem(R.id.clear).setVisible(true); -// } else { -// menu.findItem(R.id.clear).setVisible(false); -// } -// } else { -// menu.findItem(R.id.groupInfo).setVisible(false); -// menu.findItem(R.id.leaveGroup).setVisible(false); -// } + MenuItem addToContacts = menu.findItem(R.id.add_to_contacts); + MenuItem callMenu = menu.findItem(R.id.call); + MenuItem videoMenu = menu.findItem(R.id.video_call); + MenuItem keyMenu = menu.findItem(R.id.key); + MenuItem timerMenu = menu.findItem(R.id.timer); // Voice and Video calls boolean callsEnabled = ActorSDK.sharedActor().isCallsEnabled(); @@ -259,7 +261,6 @@ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { if (peer.getPeerType() == PeerType.PRIVATE) { callsEnabled = !users().get(peer.getPeerId()).isBot(); } else if (peer.getPeerType() == PeerType.GROUP) { - GroupVM groupVM = groups().get(peer.getPeerId()); if (groupVM.getGroupType() == GroupType.GROUP && groupVM.isMember().get() && groupVM.getIsCanCall().get()) { callsEnabled = groupVM.getMembersCount().get() <= MAX_USERS_FOR_CALLS; @@ -268,14 +269,21 @@ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { callsEnabled = false; videoCallsEnabled = false; } + } else { + callsEnabled = false; + videoCallsEnabled = false; } } - menu.findItem(R.id.call).setVisible(callsEnabled); - menu.findItem(R.id.video_call).setVisible(callsEnabled && videoCallsEnabled); + callMenu.setVisible(callsEnabled); + videoMenu.setVisible(callsEnabled && videoCallsEnabled); + + // Secret Chat + keyMenu.setVisible(peer.getPeerType() == PeerType.PRIVATE_ENCRYPTED); + timerMenu.setVisible(peer.getPeerType() == PeerType.PRIVATE_ENCRYPTED); // Add to contacts if (peer.getPeerType() == PeerType.PRIVATE) { - menu.findItem(R.id.add_to_contacts).setVisible(!users().get(peer.getPeerId()).isContact().get()); + addToContacts.setVisible(!users().get(peer.getPeerId()).isContact().get()); } } @@ -352,6 +360,21 @@ public void onError(final Exception e) { return true; } } + + if (item.getItemId() == R.id.timer) { + int[] timers = new int[]{ + 0, 1000, 2000, 5000, 15000, 60000, 60 * 60000, 24 * 60 * 60000 + }; + new AlertDialog.Builder(getActivity()) + .setTitle(R.string.timer_title) + .setItems(R.array.timer_values, (dialogInterface, i1) -> { + execute(messenger().setSecretChatTimer(peer.getPeerId(), timers[i1])); + }) + .show() + .setCanceledOnTouchOutside(true); + + } + return super.onOptionsItemSelected(item); } @@ -367,7 +390,7 @@ public void onRequestPermissionsResult(int requestCode, String[] permissions, in private void startCall(boolean video) { Command cmd; - if (peer.getPeerType() == PeerType.PRIVATE) { + if (peer.getPeerType() == PeerType.PRIVATE || peer.getPeerType() == PeerType.PRIVATE_ENCRYPTED) { cmd = video ? messenger().doVideoCall(peer.getPeerId()) : messenger().doCall(peer.getPeerId()); } else { cmd = messenger().doGroupCall(peer.getPeerId()); diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/dialogs/view/DialogView.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/dialogs/view/DialogView.java index 146cc812a2..df8c2a83eb 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/dialogs/view/DialogView.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/dialogs/view/DialogView.java @@ -62,8 +62,6 @@ public class DialogView extends ListItemBackgroundView 0) { - String contentText = messenger().getFormatter().formatContentText(arg.getSenderId(), - arg.getMessageType(), arg.getText().replace("\n", " "), arg.getRelatedUid(), - arg.isChannel()); + arg.getMessageType(), arg.getText().replace("\n", " "), arg.getRelatedUid(), arg.isChannel()); if (arg.getPeer().getPeerType() == PeerType.GROUP) { if (messenger().getFormatter().isLargeDialogMessage(arg.getMessageType())) { diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/profile/ProfileFragment.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/profile/ProfileFragment.java index 3efae67e6f..3fc94cfb42 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/profile/ProfileFragment.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/profile/ProfileFragment.java @@ -203,6 +203,24 @@ public View onCreateView(final LayoutInflater inflater, ViewGroup container, Bun }); + // + // New Encrypted Message + // + + View newEncryptedMessageView = res.findViewById(R.id.newEncryptedMessage); + ImageView newEncryptedMessageIcon = (ImageView) newEncryptedMessageView.findViewById(R.id.newEncryptedMessageIcon); + TextView newEncryptedMessageTitle = (TextView) newEncryptedMessageView.findViewById(R.id.newEncryptedMessageText); + { + Drawable drawable = getResources().getDrawable(R.drawable.ic_chat_black_24dp); + drawable.mutate().setColorFilter(style.getSettingsIconColor(), PorterDuff.Mode.SRC_IN); + newEncryptedMessageIcon.setImageDrawable(drawable); + newEncryptedMessageTitle.setTextColor(style.getTextPrimaryColor()); + } + newEncryptedMessageView.setOnClickListener(v -> { + startActivity(Intents.openPrivateSecretDialog(user.getId(), true, getActivity())); + }); + + // // Voice Call // diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/res/drawable-hdpi/ic_timer_off_white_24dp.png b/actor-sdk/sdk-core-android/android-sdk/src/main/res/drawable-hdpi/ic_timer_off_white_24dp.png new file mode 100644 index 0000000000..54253f14e5 Binary files /dev/null and b/actor-sdk/sdk-core-android/android-sdk/src/main/res/drawable-hdpi/ic_timer_off_white_24dp.png differ diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/res/drawable-hdpi/ic_timer_white_24dp.png b/actor-sdk/sdk-core-android/android-sdk/src/main/res/drawable-hdpi/ic_timer_white_24dp.png new file mode 100644 index 0000000000..e52e7955a4 Binary files /dev/null and b/actor-sdk/sdk-core-android/android-sdk/src/main/res/drawable-hdpi/ic_timer_white_24dp.png differ diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/res/drawable-hdpi/ic_vpn_key_white_24dp.png b/actor-sdk/sdk-core-android/android-sdk/src/main/res/drawable-hdpi/ic_vpn_key_white_24dp.png new file mode 100644 index 0000000000..2967aa0804 Binary files /dev/null and b/actor-sdk/sdk-core-android/android-sdk/src/main/res/drawable-hdpi/ic_vpn_key_white_24dp.png differ diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/res/drawable-mdpi/ic_timer_off_white_24dp.png b/actor-sdk/sdk-core-android/android-sdk/src/main/res/drawable-mdpi/ic_timer_off_white_24dp.png new file mode 100644 index 0000000000..d429b889f2 Binary files /dev/null and b/actor-sdk/sdk-core-android/android-sdk/src/main/res/drawable-mdpi/ic_timer_off_white_24dp.png differ diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/res/drawable-mdpi/ic_timer_white_24dp.png b/actor-sdk/sdk-core-android/android-sdk/src/main/res/drawable-mdpi/ic_timer_white_24dp.png new file mode 100644 index 0000000000..967cde4d20 Binary files /dev/null and b/actor-sdk/sdk-core-android/android-sdk/src/main/res/drawable-mdpi/ic_timer_white_24dp.png differ diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/res/drawable-mdpi/ic_vpn_key_white_24dp.png b/actor-sdk/sdk-core-android/android-sdk/src/main/res/drawable-mdpi/ic_vpn_key_white_24dp.png new file mode 100644 index 0000000000..2319938384 Binary files /dev/null and b/actor-sdk/sdk-core-android/android-sdk/src/main/res/drawable-mdpi/ic_vpn_key_white_24dp.png differ diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/res/drawable-xhdpi/ic_timer_off_white_24dp.png b/actor-sdk/sdk-core-android/android-sdk/src/main/res/drawable-xhdpi/ic_timer_off_white_24dp.png new file mode 100644 index 0000000000..5bdff31169 Binary files /dev/null and b/actor-sdk/sdk-core-android/android-sdk/src/main/res/drawable-xhdpi/ic_timer_off_white_24dp.png differ diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/res/drawable-xhdpi/ic_timer_white_24dp.png b/actor-sdk/sdk-core-android/android-sdk/src/main/res/drawable-xhdpi/ic_timer_white_24dp.png new file mode 100644 index 0000000000..050d50968c Binary files /dev/null and b/actor-sdk/sdk-core-android/android-sdk/src/main/res/drawable-xhdpi/ic_timer_white_24dp.png differ diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/res/drawable-xhdpi/ic_vpn_key_white_24dp.png b/actor-sdk/sdk-core-android/android-sdk/src/main/res/drawable-xhdpi/ic_vpn_key_white_24dp.png new file mode 100644 index 0000000000..18dd4bb5c1 Binary files /dev/null and b/actor-sdk/sdk-core-android/android-sdk/src/main/res/drawable-xhdpi/ic_vpn_key_white_24dp.png differ diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/res/drawable-xxhdpi/ic_timer_off_white_24dp.png b/actor-sdk/sdk-core-android/android-sdk/src/main/res/drawable-xxhdpi/ic_timer_off_white_24dp.png new file mode 100644 index 0000000000..dd81b11e7f Binary files /dev/null and b/actor-sdk/sdk-core-android/android-sdk/src/main/res/drawable-xxhdpi/ic_timer_off_white_24dp.png differ diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/res/drawable-xxhdpi/ic_timer_white_24dp.png b/actor-sdk/sdk-core-android/android-sdk/src/main/res/drawable-xxhdpi/ic_timer_white_24dp.png new file mode 100644 index 0000000000..c0f29e2d49 Binary files /dev/null and b/actor-sdk/sdk-core-android/android-sdk/src/main/res/drawable-xxhdpi/ic_timer_white_24dp.png differ diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/res/drawable-xxhdpi/ic_vpn_key_white_24dp.png b/actor-sdk/sdk-core-android/android-sdk/src/main/res/drawable-xxhdpi/ic_vpn_key_white_24dp.png new file mode 100644 index 0000000000..4deb927412 Binary files /dev/null and b/actor-sdk/sdk-core-android/android-sdk/src/main/res/drawable-xxhdpi/ic_vpn_key_white_24dp.png differ diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/res/drawable-xxxhdpi/ic_lock_black_18dp.png b/actor-sdk/sdk-core-android/android-sdk/src/main/res/drawable-xxxhdpi/ic_lock_black_18dp.png new file mode 100644 index 0000000000..b12436e848 Binary files /dev/null and b/actor-sdk/sdk-core-android/android-sdk/src/main/res/drawable-xxxhdpi/ic_lock_black_18dp.png differ diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/res/layout/fragment_profile.xml b/actor-sdk/sdk-core-android/android-sdk/src/main/res/layout/fragment_profile.xml index 8da0e0727d..a3821387d7 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/res/layout/fragment_profile.xml +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/res/layout/fragment_profile.xml @@ -136,6 +136,38 @@ + + + + + + + + + + + + + - - + + - - + + - - + + - - + + \ No newline at end of file diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/res/values/ui_text.xml b/actor-sdk/sdk-core-android/android-sdk/src/main/res/values/ui_text.xml index c7f99c741e..feb57e0a39 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/res/values/ui_text.xml +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/res/values/ui_text.xml @@ -326,6 +326,7 @@ Voice Call Video Call New Message + Secret Chat Mobile phone @@ -631,6 +632,19 @@ Share with + + Self-Destruct Timer + + Off + 1 second + 2 seconds + 5 seconds + 15 seconds + 1 minute + 1 hour + 1 day + + Unable to connect. Please make sure that you\'re connected to the Internet and try again.\n\nPlease reboot your phone if the connection problem persists. diff --git a/actor-sdk/sdk-core-ios/ActorApp/AppDelegate.swift b/actor-sdk/sdk-core-ios/ActorApp/AppDelegate.swift index 70eee0d887..1b6aef3f4e 100644 --- a/actor-sdk/sdk-core-ios/ActorApp/AppDelegate.swift +++ b/actor-sdk/sdk-core-ios/ActorApp/AppDelegate.swift @@ -23,6 +23,8 @@ import ActorSDK ActorSDK.sharedActor().enableVideoCalls = true + ActorSDK.sharedActor().enableSecretChats = true + // Setting Development Push Id ActorSDK.sharedActor().apiPushId = 868547 diff --git a/actor-sdk/sdk-core-ios/ActorSDK.xcodeproj/project.pbxproj b/actor-sdk/sdk-core-ios/ActorSDK.xcodeproj/project.pbxproj index f096cb3892..5342b436c9 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK.xcodeproj/project.pbxproj +++ b/actor-sdk/sdk-core-ios/ActorSDK.xcodeproj/project.pbxproj @@ -15,6 +15,7 @@ 060135081C95ED4C00A18C4E /* YYTransaction.m in Sources */ = {isa = PBXBuildFile; fileRef = 060135021C95ED4C00A18C4E /* YYTransaction.m */; }; 0601BBB21CA4C7DE00AEFA81 /* ElegantPresentations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0601BBB11CA4C7DE00AEFA81 /* ElegantPresentations.swift */; }; 0601BBB41CA4C80D00AEFA81 /* ElegantPresentationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0601BBB31CA4C80D00AEFA81 /* ElegantPresentationController.swift */; }; + 060DD6EE1D636675001A8333 /* AAComposeSecretController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 060DD6ED1D636675001A8333 /* AAComposeSecretController.swift */; }; 06129AA61C8359FB0099286B /* CocoaLifecycleRuntime.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06129AA51C8359FB0099286B /* CocoaLifecycleRuntime.swift */; }; 06129AA91C8394700099286B /* AAAudioManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06129AA81C8394700099286B /* AAAudioManager.swift */; }; 06129AAB1C83B80B0099286B /* AAAudioRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06129AAA1C83B80A0099286B /* AAAudioRouter.swift */; }; @@ -387,6 +388,7 @@ 060135021C95ED4C00A18C4E /* YYTransaction.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = YYTransaction.m; sourceTree = ""; }; 0601BBB11CA4C7DE00AEFA81 /* ElegantPresentations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ElegantPresentations.swift; sourceTree = ""; }; 0601BBB31CA4C80D00AEFA81 /* ElegantPresentationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ElegantPresentationController.swift; sourceTree = ""; }; + 060DD6ED1D636675001A8333 /* AAComposeSecretController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AAComposeSecretController.swift; sourceTree = ""; }; 06129AA51C8359FB0099286B /* CocoaLifecycleRuntime.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CocoaLifecycleRuntime.swift; sourceTree = ""; }; 06129AA81C8394700099286B /* AAAudioManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AAAudioManager.swift; sourceTree = ""; }; 06129AAA1C83B80A0099286B /* AAAudioRouter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AAAudioRouter.swift; sourceTree = ""; }; @@ -1288,6 +1290,7 @@ isa = PBXGroup; children = ( 066A52FA1BC52FA8000E606E /* AAComposeController.swift */, + 060DD6ED1D636675001A8333 /* AAComposeSecretController.swift */, 066A52FB1BC52FA8000E606E /* AAGroupCreateViewController.swift */, 066A52FC1BC52FA8000E606E /* AAGroupMembersController.swift */, ); @@ -2132,6 +2135,7 @@ 065974A71BC62B3600B8C7DF /* ViewExtensions.swift in Sources */, 066A52F11BC52B02000E606E /* AASettingsNotificationsViewController.swift in Sources */, 061850FF1C95CBF000C522D5 /* YYTextAsyncLayer.m in Sources */, + 060DD6EE1D636675001A8333 /* AAComposeSecretController.swift in Sources */, 065A06B61C6CEFE00012EA09 /* CocoaWebRTCRuntime.swift in Sources */, 15D35F5A1C20187E00E3717A /* AATimer.m in Sources */, 066A52381BC4EEBA000E606E /* AAHeaderCell.swift in Sources */, diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Resources/Base.lproj/Localizable.strings b/actor-sdk/sdk-core-ios/ActorSDK/Resources/Base.lproj/Localizable.strings index e54b96b6de..4d59fd807e 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Resources/Base.lproj/Localizable.strings +++ b/actor-sdk/sdk-core-ios/ActorSDK/Resources/Base.lproj/Localizable.strings @@ -694,6 +694,7 @@ "ActionUnmute" = "Unmute"; +"ActionStartSecret" = "Start Secret Chat"; "ActionDelete" = "Delete"; diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Resources/Images.xcassets/ic_secret.imageset/Contents.json b/actor-sdk/sdk-core-ios/ActorSDK/Resources/Images.xcassets/ic_secret.imageset/Contents.json new file mode 100644 index 0000000000..d8f1eb3398 --- /dev/null +++ b/actor-sdk/sdk-core-ios/ActorSDK/Resources/Images.xcassets/ic_secret.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "Private 2-64.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "Private 2-96.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Resources/Images.xcassets/ic_secret.imageset/Private 2-64.png b/actor-sdk/sdk-core-ios/ActorSDK/Resources/Images.xcassets/ic_secret.imageset/Private 2-64.png new file mode 100644 index 0000000000..a10fd6d6c5 Binary files /dev/null and b/actor-sdk/sdk-core-ios/ActorSDK/Resources/Images.xcassets/ic_secret.imageset/Private 2-64.png differ diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Resources/Images.xcassets/ic_secret.imageset/Private 2-96.png b/actor-sdk/sdk-core-ios/ActorSDK/Resources/Images.xcassets/ic_secret.imageset/Private 2-96.png new file mode 100644 index 0000000000..56fe302736 Binary files /dev/null and b/actor-sdk/sdk-core-ios/ActorSDK/Resources/Images.xcassets/ic_secret.imageset/Private 2-96.png differ diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Resources/Images.xcassets/ic_secret_title.imageset/Contents.json b/actor-sdk/sdk-core-ios/ActorSDK/Resources/Images.xcassets/ic_secret_title.imageset/Contents.json new file mode 100644 index 0000000000..41fcb0d4c3 --- /dev/null +++ b/actor-sdk/sdk-core-ios/ActorSDK/Resources/Images.xcassets/ic_secret_title.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "Lock Filled-48.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "Lock Filled-64.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Resources/Images.xcassets/ic_secret_title.imageset/Lock Filled-48.png b/actor-sdk/sdk-core-ios/ActorSDK/Resources/Images.xcassets/ic_secret_title.imageset/Lock Filled-48.png new file mode 100644 index 0000000000..5cf6abbf27 Binary files /dev/null and b/actor-sdk/sdk-core-ios/ActorSDK/Resources/Images.xcassets/ic_secret_title.imageset/Lock Filled-48.png differ diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Resources/Images.xcassets/ic_secret_title.imageset/Lock Filled-64.png b/actor-sdk/sdk-core-ios/ActorSDK/Resources/Images.xcassets/ic_secret_title.imageset/Lock Filled-64.png new file mode 100644 index 0000000000..7a92065772 Binary files /dev/null and b/actor-sdk/sdk-core-ios/ActorSDK/Resources/Images.xcassets/ic_secret_title.imageset/Lock Filled-64.png differ diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Resources/Images.xcassets/ic_timer_22.imageset/Contents.json b/actor-sdk/sdk-core-ios/ActorSDK/Resources/Images.xcassets/ic_timer_22.imageset/Contents.json new file mode 100644 index 0000000000..d69d800e77 --- /dev/null +++ b/actor-sdk/sdk-core-ios/ActorSDK/Resources/Images.xcassets/ic_timer_22.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "Timer-44.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "Timer-66.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Resources/Images.xcassets/ic_timer_22.imageset/Timer-44.png b/actor-sdk/sdk-core-ios/ActorSDK/Resources/Images.xcassets/ic_timer_22.imageset/Timer-44.png new file mode 100644 index 0000000000..9c70cb528a Binary files /dev/null and b/actor-sdk/sdk-core-ios/ActorSDK/Resources/Images.xcassets/ic_timer_22.imageset/Timer-44.png differ diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Resources/Images.xcassets/ic_timer_22.imageset/Timer-66.png b/actor-sdk/sdk-core-ios/ActorSDK/Resources/Images.xcassets/ic_timer_22.imageset/Timer-66.png new file mode 100644 index 0000000000..d25191f8c1 Binary files /dev/null and b/actor-sdk/sdk-core-ios/ActorSDK/Resources/Images.xcassets/ic_timer_22.imageset/Timer-66.png differ diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Resources/es.lproj/Localizable.strings b/actor-sdk/sdk-core-ios/ActorSDK/Resources/es.lproj/Localizable.strings index c405b02364..d3ab0fc7e9 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Resources/es.lproj/Localizable.strings +++ b/actor-sdk/sdk-core-ios/ActorSDK/Resources/es.lproj/Localizable.strings @@ -687,6 +687,9 @@ "ActionUnmute" = "No silenciado"; +"ActionStartSecret" = "Start Secret Chat"; + + "ActionDelete" = "Eliminar"; "ActionDeleteMessage" = "¿Seguro desea borrar el chat?"; diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Resources/pt.lproj/Localizable.strings b/actor-sdk/sdk-core-ios/ActorSDK/Resources/pt.lproj/Localizable.strings index e867ef01ff..c35d2d0fc4 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Resources/pt.lproj/Localizable.strings +++ b/actor-sdk/sdk-core-ios/ActorSDK/Resources/pt.lproj/Localizable.strings @@ -669,6 +669,9 @@ "ActionUnmute" = "Unmute"; +"ActionStartSecret" = "Start Secret Chat"; + + "ActionDelete" = "Delete"; "ActionDeleteMessage" = "Are you sure want to delete chat?"; diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Resources/ru.lproj/Localizable.strings b/actor-sdk/sdk-core-ios/ActorSDK/Resources/ru.lproj/Localizable.strings index 176a1b2015..bfc6510fb0 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Resources/ru.lproj/Localizable.strings +++ b/actor-sdk/sdk-core-ios/ActorSDK/Resources/ru.lproj/Localizable.strings @@ -422,6 +422,7 @@ "ComposeTitle" = "Новое сообщение"; + "CreateGroup" = "Новая группа"; "CreateChannel" = "Новый канал"; @@ -689,6 +690,8 @@ "ActionUnmute" = "Включить оповещения"; +"ActionStartSecret" = "Новый секретный чат"; + "ActionDelete" = "Удалить"; "ActionDeleteChannel" = "Удалить канал"; diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Resources/zh-Hans.lproj/Localizable.strings b/actor-sdk/sdk-core-ios/ActorSDK/Resources/zh-Hans.lproj/Localizable.strings index b2f2714c52..4601a1f14d 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Resources/zh-Hans.lproj/Localizable.strings +++ b/actor-sdk/sdk-core-ios/ActorSDK/Resources/zh-Hans.lproj/Localizable.strings @@ -694,6 +694,8 @@ "ActionUnmute" = "取消禁言"; +"ActionStartSecret" = "Start Secret Chat"; + "ActionDelete" = "删除"; diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/ActorCoreExt.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/ActorCoreExt.swift index fe8ee59a22..4ad2a85850 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/ActorCoreExt.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/ActorCoreExt.swift @@ -173,6 +173,12 @@ public extension ACPeer { return self.peerType.ordinal() == ACPeerType.PRIVATE().ordinal() } } + + public var isPrivateSecret: Bool { + get { + return self.peerType.ordinal() == ACPeerType.PRIVATE_ENCRYPTED().ordinal() + } + } } public extension ACMessage { diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/CocoaCrypto.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/CocoaCrypto.swift index 71c6adf5bf..a0bdee0fc0 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/CocoaCrypto.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/CocoaCrypto.swift @@ -11,8 +11,8 @@ class CocoaCrypto: NSObject, ARCocoaCryptoProxyProvider { return SHA256Digest() } - func createAES128WithKey(key: IOSByteArray!) -> ARBlockCipher! { - return AES128(key: key) + func createAES256WithKey(key: IOSByteArray!) -> ARBlockCipher! { + return AES256(key: key) } } @@ -55,7 +55,7 @@ class SHA256Digest: NSObject, ARDigest { } } -class AES128: NSObject, ARBlockCipher { +class AES256: NSObject, ARBlockCipher { var encryptor = UnsafeMutablePointer.alloc(1) var decryptor = UnsafeMutablePointer.alloc(1) @@ -91,7 +91,7 @@ class AES128: NSObject, ARBlockCipher { .advancedBy(Int(destOffset)) var bytesOut: Int = 0 - CCCryptorUpdate(encryptor.memory, src, 16, dst, 32, &bytesOut) + CCCryptorUpdate(encryptor.memory, src, 16, dst, 16, &bytesOut) } func decryptBlock(data: IOSByteArray!, withOffset offset: jint, toDest dest: IOSByteArray!, withOffset destOffset: jint) { @@ -102,7 +102,7 @@ class AES128: NSObject, ARBlockCipher { .advancedBy(Int(destOffset)) var bytesOut: Int = 0 - CCCryptorUpdate(decryptor.memory, src, 16, dst, 32, &bytesOut) + CCCryptorUpdate(decryptor.memory, src, 16, dst, 16, &bytesOut) } func getBlockSize() -> jint { diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/CocoaStorageRuntime.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/CocoaStorageRuntime.swift index 18c18f7bbc..2e7c7fc71c 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/CocoaStorageRuntime.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/CocoaStorageRuntime.swift @@ -6,12 +6,12 @@ import Foundation @objc class CocoaStorageRuntime : NSObject, ARStorageRuntime { - let dbPath: String; + let dbQueue: FMDatabaseQueue let preferences = UDPreferencesStorage() override init() { - self.dbPath = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, - .UserDomainMask, true)[0].asNS.stringByAppendingPathComponent("actor.db") + dbQueue = FMDatabaseQueue(path: NSSearchPathForDirectoriesInDomains(.DocumentDirectory, + .UserDomainMask, true)[0].asNS.stringByAppendingPathComponent("actor.db")) } func createPreferencesStorage() -> ARPreferencesStorage! { @@ -19,19 +19,17 @@ import Foundation } func createKeyValueWithName(name: String!) -> ARKeyValueStorage! { - return FMDBKeyValue(databasePath: dbPath, tableName: name) + return FMDBKeyValue(dbQueue: dbQueue, tableName: name) } func createListWithName(name: String!) -> ARListStorage! { - return FMDBList(databasePath: dbPath, tableName: name) + return FMDBList(dbQueue: dbQueue, tableName: name) } func resetStorage() { preferences.clear() - - let db = FMDatabase(path: dbPath) - db.open() - db.executeStatements("select 'drop table ' || name || ';' from sqlite_master where type = 'table';") - db.close() + dbQueue.inDatabase { (db) in + db.executeStatements("select 'drop table ' || name || ';' from sqlite_master where type = 'table';") + } } } \ No newline at end of file diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/Storage/FMDBKeyValue.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/Storage/FMDBKeyValue.swift index 982e1ab713..ba302befd9 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/Storage/FMDBKeyValue.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/Storage/FMDBKeyValue.swift @@ -6,9 +6,8 @@ import Foundation @objc class FMDBKeyValue: NSObject, ARKeyValueStorage { - var db :FMDatabase! + let dbQueue: FMDatabaseQueue - let databasePath: String let tableName: String let queryCreate: String @@ -21,8 +20,8 @@ import Foundation var isTableChecked: Bool = false - init(databasePath: String, tableName: String) { - self.databasePath = databasePath + init(dbQueue: FMDatabaseQueue, tableName: String) { + self.dbQueue = dbQueue self.tableName = tableName // Queries @@ -46,72 +45,76 @@ import Foundation } isTableChecked = true - self.db = FMDatabase(path: databasePath) - self.db.open() - if (!db.tableExists(tableName)) { - db.executeUpdate(queryCreate) + dbQueue.inDatabase { (db) in + if (!db.tableExists(self.tableName)) { + db.executeUpdate(self.queryCreate) + } } } func addOrUpdateItems(values: JavaUtilList!) { checkTable() - db.beginTransaction() - for i in 0.. IOSByteArray! { checkTable() - let result = db.dataForQuery(queryItem, key.toNSNumber()) - if (result == nil) { - return nil + var res: IOSByteArray! = nil + dbQueue.inDatabase { (db) in + let result = db.dataForQuery(self.queryItem, key.toNSNumber()) + if (result == nil) { + return + } + res = result.toJavaBytes() } - return result.toJavaBytes() + return res } func loadAllItems() -> JavaUtilList! { checkTable() let res = JavaUtilArrayList() - - if let result = db.executeQuery(queryAll) { - while(result.next()) { - res.addWithId(ARKeyValueRecord(key: jlong(result.longLongIntForColumn("ID")), withData: result.dataForColumn("BYTES").toJavaBytes())) + dbQueue.inDatabase { (db) in + if let result = db.executeQuery(self.queryAll) { + while(result.next()) { + res.addWithId(ARKeyValueRecord(key: jlong(result.longLongIntForColumn("ID")), withData: result.dataForColumn("BYTES").toJavaBytes())) + } } } - return res } @@ -125,22 +128,22 @@ import Foundation } let res = JavaUtilArrayList() - - if let result = db.executeQuery(queryItems, ids) { - while(result.next()) { - // TODO: Optimize lookup - res.addWithId(ARKeyValueRecord(key: jlong(result.longLongIntForColumn("ID")), withData: result.dataForColumn("BYTES").toJavaBytes())) + dbQueue.inDatabase { (db) in + if let result = db.executeQuery(self.queryItems, ids) { + while(result.next()) { + // TODO: Optimize lookup + res.addWithId(ARKeyValueRecord(key: jlong(result.longLongIntForColumn("ID")), withData: result.dataForColumn("BYTES").toJavaBytes())) + } } } - return res } func clear() { checkTable() - db.beginTransaction() - db.executeUpdate(queryDeleteAll) - db.commit() + dbQueue.inTransaction { (db, rollout) in + db.executeUpdate(self.queryDeleteAll) + } } } \ No newline at end of file diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/Storage/FMDBList.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/Storage/FMDBList.swift index c775773f2b..0a1439bc8d 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/Storage/FMDBList.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/Storage/FMDBList.swift @@ -6,73 +6,72 @@ import Foundation class FMDBList : NSObject, ARListStorageDisplayEx { - var db :FMDatabase? = nil; - var isTableChecked: Bool = false; + let dbQueue: FMDatabaseQueue + let tableName: String - let databasePath: String; - let tableName: String; - - let queryCreate: String; - let queryCreateIndex: String; - let queryCreateFilter: String; + var isTableChecked: Bool = false + + let queryCreate: String + let queryCreateIndex: String + let queryCreateFilter: String - let queryCount: String; + let queryCount: String let queryEmpty: String - let queryAdd: String; - let queryItem: String; + let queryAdd: String + let queryItem: String - let queryDelete: String; - let queryDeleteAll: String; + let queryDelete: String + let queryDeleteAll: String - let queryForwardFirst: String; - let queryForwardMore: String; + let queryForwardFirst: String + let queryForwardMore: String - let queryForwardFilterFirst: String; - let queryForwardFilterMore: String; + let queryForwardFilterFirst: String + let queryForwardFilterMore: String - let queryBackwardFirst: String; - let queryBackwardMore: String; - let queryBackwardFilterFirst: String; - let queryBackwardFilterMore: String; + let queryBackwardFirst: String + let queryBackwardMore: String + let queryBackwardFilterFirst: String + let queryBackwardFilterMore: String - let queryCenterBackward: String; - let queryCenterForward: String; + let queryCenterBackward: String + let queryCenterForward: String - init (databasePath: String, tableName: String){ - self.databasePath = databasePath - self.tableName = tableName; + init (dbQueue: FMDatabaseQueue, tableName: String){ + self.dbQueue = dbQueue + self.tableName = tableName self.queryCreate = "CREATE TABLE IF NOT EXISTS " + tableName + " (" + // "\"ID\" INTEGER NOT NULL," + // 0: id "\"SORT_KEY\" INTEGER NOT NULL," + // 1: sortKey "\"QUERY\" TEXT," + // 2: query "\"BYTES\" BLOB NOT NULL," + // 3: bytes - "PRIMARY KEY(\"ID\"));"; + "PRIMARY KEY(\"ID\"));" self.queryCreateIndex = "CREATE INDEX IF NOT EXISTS IDX_ID_SORT ON " + tableName + " (\"SORT_KEY\");" self.queryCreateFilter = "CREATE INDEX IF NOT EXISTS IDX_ID_QUERY_SORT ON " + tableName + " (\"QUERY\", \"SORT_KEY\");" - self.queryCount = "SELECT COUNT(*) FROM " + tableName + ";"; + self.queryCount = "SELECT COUNT(*) FROM " + tableName + ";" self.queryEmpty = "EXISTS (SELECT * FROM " + tableName + ");" - self.queryAdd = "REPLACE INTO " + tableName + " (\"ID\",\"QUERY\",\"SORT_KEY\",\"BYTES\") VALUES (?,?,?,?)"; - self.queryItem = "SELECT \"ID\",\"QUERY\",\"SORT_KEY\",\"BYTES\" FROM " + tableName + " WHERE \"ID\" = ?;"; + self.queryAdd = "REPLACE INTO " + tableName + " (\"ID\",\"QUERY\",\"SORT_KEY\",\"BYTES\") VALUES (?,?,?,?)" + self.queryItem = "SELECT \"ID\",\"QUERY\",\"SORT_KEY\",\"BYTES\" FROM " + tableName + " WHERE \"ID\" = ?;" - self.queryDeleteAll = "DELETE FROM " + tableName + ";"; - self.queryDelete = "DELETE FROM " + tableName + " WHERE \"ID\"= ?;"; + self.queryDeleteAll = "DELETE FROM " + tableName + ";" + self.queryDelete = "DELETE FROM " + tableName + " WHERE \"ID\"= ?;" - self.queryForwardFirst = "SELECT \"ID\", \"QUERY\",\"SORT_KEY\", \"BYTES\" FROM " + tableName + " ORDER BY SORT_KEY DESC LIMIT ?"; - self.queryForwardMore = "SELECT \"ID\", \"QUERY\",\"SORT_KEY\", \"BYTES\" FROM " + tableName + " WHERE \"SORT_KEY\" < ? ORDER BY SORT_KEY DESC LIMIT ?"; + self.queryForwardFirst = "SELECT \"ID\", \"QUERY\",\"SORT_KEY\", \"BYTES\" FROM " + tableName + " ORDER BY SORT_KEY DESC LIMIT ?" + self.queryForwardMore = "SELECT \"ID\", \"QUERY\",\"SORT_KEY\", \"BYTES\" FROM " + tableName + " WHERE \"SORT_KEY\" < ? ORDER BY SORT_KEY DESC LIMIT ?" - self.queryBackwardFirst = "SELECT \"ID\", \"QUERY\",\"SORT_KEY\", \"BYTES\" FROM " + tableName + " ORDER BY SORT_KEY ASC LIMIT ?"; - self.queryBackwardMore = "SELECT \"ID\", \"QUERY\",\"SORT_KEY\", \"BYTES\" FROM " + tableName + " WHERE \"SORT_KEY\" > ? ORDER BY SORT_KEY ASC LIMIT ?"; + self.queryBackwardFirst = "SELECT \"ID\", \"QUERY\",\"SORT_KEY\", \"BYTES\" FROM " + tableName + " ORDER BY SORT_KEY ASC LIMIT ?" + self.queryBackwardMore = "SELECT \"ID\", \"QUERY\",\"SORT_KEY\", \"BYTES\" FROM " + tableName + " WHERE \"SORT_KEY\" > ? ORDER BY SORT_KEY ASC LIMIT ?" self.queryCenterForward = queryForwardMore - self.queryCenterBackward = "SELECT \"ID\", \"QUERY\",\"SORT_KEY\", \"BYTES\" FROM " + tableName + " WHERE \"SORT_KEY\" >= ? ORDER BY SORT_KEY ASC LIMIT ?"; + self.queryCenterBackward = "SELECT \"ID\", \"QUERY\",\"SORT_KEY\", \"BYTES\" FROM " + tableName + " WHERE \"SORT_KEY\" >= ? ORDER BY SORT_KEY ASC LIMIT ?" - self.queryForwardFilterFirst = "SELECT \"ID\", \"QUERY\",\"SORT_KEY\", \"BYTES\" FROM " + tableName + " WHERE \"QUERY\" LIKE ? OR \"QUERY\" LIKE ? ORDER BY SORT_KEY DESC LIMIT ?"; - self.queryForwardFilterMore = "SELECT \"ID\", \"QUERY\",\"SORT_KEY\", \"BYTES\" FROM " + tableName + " WHERE (\"QUERY\" LIKE ? OR \"QUERY\" LIKE ?) AND \"SORT_KEY\" < ? ORDER BY SORT_KEY DESC LIMIT ?"; + self.queryForwardFilterFirst = "SELECT \"ID\", \"QUERY\",\"SORT_KEY\", \"BYTES\" FROM " + tableName + " WHERE \"QUERY\" LIKE ? OR \"QUERY\" LIKE ? ORDER BY SORT_KEY DESC LIMIT ?" + self.queryForwardFilterMore = "SELECT \"ID\", \"QUERY\",\"SORT_KEY\", \"BYTES\" FROM " + tableName + " WHERE (\"QUERY\" LIKE ? OR \"QUERY\" LIKE ?) AND \"SORT_KEY\" < ? ORDER BY SORT_KEY DESC LIMIT ?" - self.queryBackwardFilterFirst = "SELECT \"ID\", \"QUERY\",\"SORT_KEY\", \"BYTES\" FROM " + tableName + " WHERE \"QUERY\" LIKE ? OR \"QUERY\" LIKE ? ORDER BY SORT_KEY ASC LIMIT ?"; - self.queryBackwardFilterMore = "SELECT \"ID\", \"QUERY\",\"SORT_KEY\", \"BYTES\" FROM " + tableName + " WHERE (\"QUERY\" LIKE ? OR \"QUERY\" LIKE ?) AND \"SORT_KEY\" > ? ORDER BY SORT_KEY ASC LIMIT ?"; + self.queryBackwardFilterFirst = "SELECT \"ID\", \"QUERY\",\"SORT_KEY\", \"BYTES\" FROM " + tableName + " WHERE \"QUERY\" LIKE ? OR \"QUERY\" LIKE ? ORDER BY SORT_KEY ASC LIMIT ?" + self.queryBackwardFilterMore = "SELECT \"ID\", \"QUERY\",\"SORT_KEY\", \"BYTES\" FROM " + tableName + " WHERE (\"QUERY\" LIKE ? OR \"QUERY\" LIKE ?) AND \"SORT_KEY\" > ? ORDER BY SORT_KEY ASC LIMIT ?" } func checkTable() { @@ -81,122 +80,126 @@ class FMDBList : NSObject, ARListStorageDisplayEx { } isTableChecked = true; - self.db = FMDatabase(path: databasePath) - self.db!.open() - if (!db!.tableExists(tableName)) { - db!.executeUpdate(queryCreate) - db!.executeUpdate(queryCreateIndex) - db!.executeUpdate(queryCreateFilter) + dbQueue.inDatabase { (db) in + if (!db!.tableExists(self.tableName)) { + db!.executeUpdate(self.queryCreate) + db!.executeUpdate(self.queryCreateIndex) + db!.executeUpdate(self.queryCreateFilter) + } } } func updateOrAddWithValue(valueContainer: ARListEngineRecord!) { - checkTable(); + checkTable() let start = NSDate() - // db!.beginTransaction() - db!.executeUpdate(queryAdd, withArgumentsInArray: [valueContainer.getKey().toNSNumber(), valueContainer.dbQuery(), valueContainer.getOrder().toNSNumber(), - valueContainer.getData().toNSData()]) - // db!.commit() + dbQueue.inDatabase { (db) in + db.executeUpdate(self.queryAdd, withArgumentsInArray: [valueContainer.getKey().toNSNumber(), valueContainer.dbQuery(), valueContainer.getOrder().toNSNumber(), + valueContainer.getData().toNSData()]) + + } log("updateOrAddWithValue \(tableName): \(valueContainer.getData().length()) in \(Int((NSDate().timeIntervalSinceDate(start)*1000)))") } func updateOrAddWithList(items: JavaUtilList!) { - checkTable(); + checkTable() - db!.beginTransaction() - for i in 0.. jint { - checkTable(); + checkTable() - let result = db!.executeQuery(queryCount) - if (result == nil) { - return 0; - } - if (result!.next()) { - let res = jint(result!.intForColumnIndex(0)) - result?.close() - return res - } else { - result?.close() + var res: jint = 0 + dbQueue.inDatabase { (db) in + let result = db.executeQuery(self.queryCount) + if (result == nil) { + return; + } + if (result!.next()) { + res = jint(result!.intForColumnIndex(0)) + result?.close() + } else { + result?.close() + } } - - return 0; + return res; } func isEmpty() -> Bool { - checkTable(); + checkTable() - let result = db!.executeQuery(queryEmpty) - if (result == nil) { - return false; - } - if (result!.next()) { - let res = result!.intForColumnIndex(0) - result?.close() - return res > 0 - } else { + var res: Bool = false + dbQueue.inDatabase { (db) in + let result = db!.executeQuery(self.queryEmpty) + if (result == nil) { + return + } + if (result!.next()) { + res = result!.intForColumnIndex(0) > 0 + } result?.close() } - - return false; + return res } func clear() { - checkTable(); + checkTable() - db!.beginTransaction() - db!.executeUpdate(queryDeleteAll); - db!.commit() + dbQueue.inTransaction { (db, rollout) in + db.executeUpdate(self.queryDeleteAll) + } } func loadItemWithKey(key: jlong) -> ARListEngineRecord! { - checkTable(); + checkTable() - let result = db!.executeQuery(queryItem, key.toNSNumber()); - if (result == nil) { - return nil - } - if (result!.next()) { - var query: AnyObject! = result!.objectForColumnName("QUERY"); - if (query is NSNull){ - query = nil + var res: ARListEngineRecord! = nil + + dbQueue.inDatabase { (db) in + let result = db!.executeQuery(self.queryItem, key.toNSNumber()) + if (result == nil) { + return + } + if (result!.next()) { + var query: AnyObject! = result!.objectForColumnName("QUERY") + if (query is NSNull){ + query = nil + } + res = ARListEngineRecord(key: jlong(result!.longLongIntForColumn("ID")), withOrder: jlong(result!.longLongIntForColumn("SORT_KEY")), withQuery: query as! String?, withData: result!.dataForColumn("BYTES").toJavaBytes()) } - let res = ARListEngineRecord(key: jlong(result!.longLongIntForColumn("ID")), withOrder: jlong(result!.longLongIntForColumn("SORT_KEY")), withQuery: query as! String?, withData: result!.dataForColumn("BYTES").toJavaBytes()) - result?.close() - return res; - } else { result?.close() - return nil } + + + return res } func loadAllItems() -> JavaUtilList! { @@ -206,152 +209,155 @@ class FMDBList : NSObject, ARListStorageDisplayEx { } func loadForwardWithSortKey(sortingKey: JavaLangLong!, withLimit limit: jint) -> JavaUtilList! { - checkTable(); - var result : FMResultSet? = nil; - if (sortingKey == nil) { - result = db!.executeQuery(queryForwardFirst, limit.toNSNumber()); - } else { - result = db!.executeQuery(queryForwardMore, sortingKey!.toNSNumber(), limit.toNSNumber()); - } + checkTable() - if (result == nil) { - NSLog(db!.lastErrorMessage()) - return nil - } - - let res: JavaUtilArrayList = JavaUtilArrayList(); - - let queryIndex = result!.columnIndexForName("QUERY") - let idIndex = result!.columnIndexForName("ID") - let sortKeyIndex = result!.columnIndexForName("SORT_KEY") - let bytesIndex = result!.columnIndexForName("BYTES") - var dataSize = 0 - var rowCount = 0 - - while(result!.next()) { - let key = jlong(result!.longLongIntForColumnIndex(idIndex)) - let order = jlong(result!.longLongIntForColumnIndex(sortKeyIndex)) - var query: AnyObject! = result!.objectForColumnIndex(queryIndex) - if (query is NSNull) { - query = nil + let res = JavaUtilArrayList() + dbQueue.inDatabase { (db) in + var result : FMResultSet? = nil + if (sortingKey == nil) { + result = db!.executeQuery(self.queryForwardFirst, limit.toNSNumber()) + } else { + result = db!.executeQuery(self.queryForwardMore, sortingKey!.toNSNumber(), limit.toNSNumber()) } - let data = result!.dataForColumnIndex(bytesIndex).toJavaBytes() - dataSize += Int(data.length()) - rowCount += 1 - let record = ARListEngineRecord(key: key, withOrder: order, withQuery: query as! String?, withData: data) - res.addWithId(record) + if (result == nil) { + NSLog(db!.lastErrorMessage()) + return + } + let queryIndex = result!.columnIndexForName("QUERY") + let idIndex = result!.columnIndexForName("ID") + let sortKeyIndex = result!.columnIndexForName("SORT_KEY") + let bytesIndex = result!.columnIndexForName("BYTES") + var dataSize = 0 + var rowCount = 0 + + while(result!.next()) { + let key = jlong(result!.longLongIntForColumnIndex(idIndex)) + let order = jlong(result!.longLongIntForColumnIndex(sortKeyIndex)) + var query: AnyObject! = result!.objectForColumnIndex(queryIndex) + if (query is NSNull) { + query = nil + } + let data = result!.dataForColumnIndex(bytesIndex).toJavaBytes() + dataSize += Int(data.length()) + rowCount += 1 + + let record = ARListEngineRecord(key: key, withOrder: order, withQuery: query as! String?, withData: data) + res.addWithId(record) + } + result!.close() } - result!.close() - - return res; + return res } func loadForwardWithQuery(query: String!, withSortKey sortingKey: JavaLangLong!, withLimit limit: jint) -> JavaUtilList! { - checkTable(); + checkTable() - var result : FMResultSet? = nil; - if (sortingKey == nil) { - result = db!.executeQuery(queryForwardFilterFirst, query + "%", "% " + query + "%", limit.toNSNumber()); - } else { - result = db!.executeQuery(queryForwardFilterMore, query + "%", "% " + query + "%", sortingKey!.toNSNumber(), limit.toNSNumber()); - } - if (result == nil) { - NSLog(db!.lastErrorMessage()) - return nil - } - - let res: JavaUtilArrayList = JavaUtilArrayList(); - - while(result!.next()) { - var query: AnyObject! = result!.objectForColumnName("QUERY"); - if (query is NSNull) { - query = nil + let res = JavaUtilArrayList() + dbQueue.inDatabase { (db) in + var result : FMResultSet? = nil + if (sortingKey == nil) { + result = db!.executeQuery(self.queryForwardFilterFirst, query + "%", "% " + query + "%", limit.toNSNumber()) + } else { + result = db!.executeQuery(self.queryForwardFilterMore, query + "%", "% " + query + "%", sortingKey!.toNSNumber(), limit.toNSNumber()) } - let record = ARListEngineRecord(key: jlong(result!.longLongIntForColumn("ID")), withOrder: jlong(result!.longLongIntForColumn("SORT_KEY")), withQuery: query as! String?, withData: result!.dataForColumn("BYTES").toJavaBytes()) - res.addWithId(record) + if (result == nil) { + NSLog(db!.lastErrorMessage()) + return + } + + while(result!.next()) { + var query: AnyObject! = result!.objectForColumnName("QUERY") + if (query is NSNull) { + query = nil + } + let record = ARListEngineRecord(key: jlong(result!.longLongIntForColumn("ID")), withOrder: jlong(result!.longLongIntForColumn("SORT_KEY")), withQuery: query as! String?, withData: result!.dataForColumn("BYTES").toJavaBytes()) + res.addWithId(record) + } + result!.close() } - result!.close() - - return res; - + return res } func loadBackwardWithSortKey(sortingKey: JavaLangLong!, withLimit limit: jint) -> JavaUtilList! { - checkTable(); - var result : FMResultSet? = nil; - if (sortingKey == nil) { - result = db!.executeQuery(queryBackwardFirst, limit.toNSNumber()); - } else { - result = db!.executeQuery(queryBackwardMore, sortingKey!.toNSNumber(), limit.toNSNumber()); - } - if (result == nil) { - NSLog(db!.lastErrorMessage()) - return nil - } + checkTable() - let res: JavaUtilArrayList = JavaUtilArrayList(); - - while(result!.next()) { - var query: AnyObject! = result!.objectForColumnName("QUERY"); - if (query is NSNull) { - query = nil + let res = JavaUtilArrayList() + dbQueue.inDatabase { (db) in + var result : FMResultSet? = nil + if (sortingKey == nil) { + result = db!.executeQuery(self.queryBackwardFirst, limit.toNSNumber()) + } else { + result = db!.executeQuery(self.queryBackwardMore, sortingKey!.toNSNumber(), limit.toNSNumber()) } - let record = ARListEngineRecord(key: jlong(result!.longLongIntForColumn("ID")), withOrder: jlong(result!.longLongIntForColumn("SORT_KEY")), withQuery: query as! String?, withData: result!.dataForColumn("BYTES").toJavaBytes()) - res.addWithId(record) + if (result == nil) { + NSLog(db!.lastErrorMessage()) + return + } + + while(result!.next()) { + var query: AnyObject! = result!.objectForColumnName("QUERY") + if (query is NSNull) { + query = nil + } + let record = ARListEngineRecord(key: jlong(result!.longLongIntForColumn("ID")), withOrder: jlong(result!.longLongIntForColumn("SORT_KEY")), withQuery: query as! String?, withData: result!.dataForColumn("BYTES").toJavaBytes()) + res.addWithId(record) + } + result!.close() } - result!.close() - return res; + return res } func loadBackwardWithQuery(query: String!, withSortKey sortingKey: JavaLangLong!, withLimit limit: jint) -> JavaUtilList! { - checkTable(); - - var result : FMResultSet? = nil; - if (sortingKey == nil) { - result = db!.executeQuery(queryBackwardFilterFirst, query + "%", "% " + query + "%", limit.toNSNumber()); - } else { - result = db!.executeQuery(queryBackwardFilterMore, query + "%", "% " + query + "%", sortingKey!.toNSNumber(), limit.toNSNumber()); - } - if (result == nil) { - NSLog(db!.lastErrorMessage()) - return nil - } + checkTable() - let res: JavaUtilArrayList = JavaUtilArrayList(); - - while(result!.next()) { - var query: AnyObject! = result!.objectForColumnName("QUERY"); - if (query is NSNull) { - query = nil + let res = JavaUtilArrayList() + dbQueue.inDatabase { (db) in + var result : FMResultSet? = nil + if (sortingKey == nil) { + result = db!.executeQuery(self.queryBackwardFilterFirst, query + "%", "% " + query + "%", limit.toNSNumber()) + } else { + result = db!.executeQuery(self.queryBackwardFilterMore, query + "%", "% " + query + "%", sortingKey!.toNSNumber(), limit.toNSNumber()) } - let record = ARListEngineRecord(key: jlong(result!.longLongIntForColumn("ID")), withOrder: jlong(result!.longLongIntForColumn("SORT_KEY")), withQuery: query as! String?, withData: result!.dataForColumn("BYTES").toJavaBytes()) - res.addWithId(record) + if (result == nil) { + NSLog(db!.lastErrorMessage()) + return + } + + while(result!.next()) { + var query: AnyObject! = result!.objectForColumnName("QUERY") + if (query is NSNull) { + query = nil + } + let record = ARListEngineRecord(key: jlong(result!.longLongIntForColumn("ID")), withOrder: jlong(result!.longLongIntForColumn("SORT_KEY")), withQuery: query as! String?, withData: result!.dataForColumn("BYTES").toJavaBytes()) + res.addWithId(record) + } + result!.close() } - result!.close() - - return res; + return res } func loadCenterWithSortKey(centerSortKey: JavaLangLong!, withLimit limit: jint) -> JavaUtilList! { - checkTable(); + checkTable() - let res: JavaUtilArrayList = JavaUtilArrayList(); - res.addAllWithJavaUtilCollection(loadSlise(db!.executeQuery(queryCenterBackward, centerSortKey.toNSNumber(), limit.toNSNumber()))) - res.addAllWithJavaUtilCollection(loadSlise(db!.executeQuery(queryCenterForward, centerSortKey.toNSNumber(), limit.toNSNumber()))) + let res: JavaUtilArrayList = JavaUtilArrayList() + dbQueue.inDatabase { (db) in + res.addAllWithJavaUtilCollection(self.loadSlise(db.executeQuery(self.queryCenterBackward, centerSortKey.toNSNumber(), limit.toNSNumber()), db: db)) + res.addAllWithJavaUtilCollection(self.loadSlise(db.executeQuery(self.queryCenterForward, centerSortKey.toNSNumber(), limit.toNSNumber()), db: db)) + } return res } - func loadSlise(result: FMResultSet?) -> JavaUtilList! { + func loadSlise(result: FMResultSet?, db: FMDatabase) -> JavaUtilList! { if (result == nil) { - NSLog(db!.lastErrorMessage()) + NSLog(db.lastErrorMessage()) return nil } - let res: JavaUtilArrayList = JavaUtilArrayList(); + let res: JavaUtilArrayList = JavaUtilArrayList() while(result!.next()) { - var query: AnyObject! = result!.objectForColumnName("QUERY"); + var query: AnyObject! = result!.objectForColumnName("QUERY") if (query is NSNull) { query = nil } @@ -359,6 +365,6 @@ class FMDBList : NSObject, ARListStorageDisplayEx { res.addWithId(record) } result!.close() - return res; + return res } } \ No newline at end of file diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorSDK.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorSDK.swift index 0116857588..60c7569305 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorSDK.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorSDK.swift @@ -132,6 +132,9 @@ import DZNWebViewController /// Enable video calls feature public var enableVideoCalls: Bool = false + /// Enable secret chats feature + public var enableSecretChats: Bool = false + /// Enable custom sound on Groups and Chats public var enableChatGroupSound: Bool = false @@ -282,7 +285,7 @@ import DZNWebViewController builder.setVoiceCallsEnabled(jboolean(enableCalls)) builder.setVideoCallsEnabled(jboolean(enableCalls)) builder.setIsEnabledGroupedChatList(false) - // builder.setEnableFilesLogging(true) + builder.setEnableFilesLogging(true) // Creating messenger messenger = ACCocoaMessenger(configuration: builder.build()) diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorStyle.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorStyle.swift index 807674f9b6..a95f1b74ae 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorStyle.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorStyle.swift @@ -44,6 +44,8 @@ public class ActorStyle { public var vcBgColor = UIColor.whiteColor() /// View Controller background color for settings public var vcBackyardColor = UIColor(rgb: 0xf0eff5) + /// App's secret chats color + public var vcSecretColor: UIColor = UIColor(rgb: 0x32914d) // // UINavigationBar @@ -56,6 +58,13 @@ public class ActorStyle { public var navigationTintColor: UIColor = UIColor(rgb: 0x5085CB) /// Navigation Bar title color public var navigationTitleColor: UIColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0xDE/255.0) + /// Navigation Bar title secret color + private var _navigationTitleSecretColor: UIColor? + public var navigationTitleSecretColor: UIColor { + get { return _navigationTitleSecretColor != nil ? _navigationTitleSecretColor! : vcSecretColor } + set(v) { _navigationTitleSecretColor = v } + } + /// Navigation Bar subtitle color, default is 0.8 alhpa of navigationTitleColor public var navigationSubtitleColor: UIColor { get { return _navigationSubtitleColor != nil ? _navigationSubtitleColor! : navigationTitleColor.alpha(0.8) } @@ -539,6 +548,12 @@ public class ActorStyle { set(v) { _dialogTitleColor = v } } + private var _dialogTitleSecureColor: UIColor? + public var dialogTitleSecureColor: UIColor { + get { return _dialogTitleSecureColor != nil ? _dialogTitleSecureColor! : vcSecretColor } + set(v) { _dialogTitleSecureColor = v } + } + private var _dialogTextColor: UIColor? public var dialogTextColor: UIColor { get { return _dialogTextColor != nil ? _dialogTextColor! : dialogTitleColor.alpha(0.64) } diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Compose/AAComposeController.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Compose/AAComposeController.swift index 5640467db6..4ff995c320 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Compose/AAComposeController.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Compose/AAComposeController.swift @@ -24,6 +24,23 @@ public class AAComposeController: AAContactsListContentController, AAContactsLis } public func willAddContacts(controller: AAContactsListContentController, section: AAManagedSection) { + + if ActorSDK.sharedActor().enableSecretChats { + section.custom { (r:AACustomRow) -> () in + + r.height = 56 + + r.closure = { (cell) -> () in + cell.bind("ic_secret", actionTitle: AALocalized("ActionStartSecret")) + } + + r.selectAction = { () -> Bool in + self.navigateNext(AAComposeSecretController(), removeCurrent: true) + return false + } + } + } + section.custom { (r:AACustomRow) -> () in r.height = 56 @@ -54,10 +71,11 @@ public class AAComposeController: AAContactsListContentController, AAContactsLis } public func contactDidTap(controller: AAContactsListContentController, contact: ACContact) -> Bool { - if let customController = ActorSDK.sharedActor().delegate.actorControllerForConversation(ACPeer_userWithInt_(contact.uid)) { + let peer = ACPeer_userWithInt_(contact.uid) + if let customController = ActorSDK.sharedActor().delegate.actorControllerForConversation(peer) { navigateDetail(customController) } else { - navigateDetail(ConversationViewController(peer: ACPeer_userWithInt_(contact.uid))) + navigateDetail(ConversationViewController(peer: peer)) } return false } diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Compose/AAComposeSecretController.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Compose/AAComposeSecretController.swift new file mode 100644 index 0000000000..1a66acc5de --- /dev/null +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Compose/AAComposeSecretController.swift @@ -0,0 +1,35 @@ +// +// Copyright (c) 2014-2016 Actor LLC. +// + +import UIKit + +public class AAComposeSecretController: AAContactsListContentController, AAContactsListContentControllerDelegate { + + public override init() { + super.init() + + self.delegate = self + self.isSearchAutoHide = false + + self.navigationItem.title = AALocalized("ComposeTitle") + + if AADevice.isiPad { + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: AALocalized("NavigationCancel"), style: UIBarButtonItemStyle.Plain, target: self, action: #selector(AAViewController.dismiss)) + } + } + + public required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public func contactDidTap(controller: AAContactsListContentController, contact: ACContact) -> Bool { + let peer = ACPeer_secretWithInt_(contact.uid) + if let customController = ActorSDK.sharedActor().delegate.actorControllerForConversation(peer) { + navigateDetail(customController) + } else { + navigateDetail(ConversationViewController(peer: peer)) + } + return false + } +} \ No newline at end of file diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Contacts/AAContactsViewController.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Contacts/AAContactsViewController.swift index 390efcae58..1a5f52f6ae 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Contacts/AAContactsViewController.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Contacts/AAContactsViewController.swift @@ -47,6 +47,22 @@ public class AAContactsViewController: AAContactsListContentController, AAContac public func willAddContacts(controller: AAContactsListContentController, section: AAManagedSection) { + if ActorSDK.sharedActor().enableSecretChats { + section.custom { (r:AACustomRow) -> () in + + r.height = 56 + + r.closure = { (cell) -> () in + cell.bind("ic_secret", actionTitle: AALocalized("ActionStartSecret")) + } + + r.selectAction = { () -> Bool in + self.navigateNext(AAComposeSecretController(), removeCurrent: false) + return false + } + } + } + section.custom { (r: AACustomRow) -> () in r.height = 56 diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Cell/AABubbleBaseFileCell.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Cell/AABubbleBaseFileCell.swift index e8edb7ed1b..8f8c5702a9 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Cell/AABubbleBaseFileCell.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Cell/AABubbleBaseFileCell.swift @@ -90,7 +90,7 @@ public class AABubbleBaseFileCell: AABubbleCell { self.fileStateChanged(reference, progress: nil, isPaused: false, isUploading: false, selfGeneration: selfGeneration) }) - Actor.bindRawFileWithReference(ACFileReference(ARApiFileLocation: file.reference.getFileLocation(), withNSString: file.reference.fileName, withInt: file.reference.fileSize), autoStart: autoDownload, withCallback: bindedDownloadCallback) + Actor.bindRawFileWithReference(file.reference, autoStart: autoDownload, withCallback: bindedDownloadCallback) } else { fatalError("Unsupported message type") } diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Dialogs List/Cells/AADialogCell.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Dialogs List/Cells/AADialogCell.swift index f86667f9a8..8ec44c4e9b 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Dialogs List/Cells/AADialogCell.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Dialogs List/Cells/AADialogCell.swift @@ -255,7 +255,12 @@ public class AADialogCell: AATableViewCell, AABindedCell { let title = NSMutableAttributedString(string: config.item.dialogTitle) title.yy_font = UIFont.mediumSystemFontOfSize(17) - title.yy_color = appStyle.dialogTitleColor + if config.item.peer.isPrivateSecret { + title.yy_color = appStyle.dialogTitleSecureColor + } else { + title.yy_color = appStyle.dialogTitleColor + } + let titleContainer = YYTextContainer(size: CGSize(width: config.titleWidth, height: 1000)) titleContainer.maximumNumberOfRows = 1 titleContainer.truncationType = .End diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Conversation/ConversationViewController.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Conversation/ConversationViewController.swift index 9fe626f1ad..a57941fa6f 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Conversation/ConversationViewController.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Conversation/ConversationViewController.swift @@ -205,7 +205,11 @@ public class ConversationViewController: titleView.textAlignment = NSTextAlignment.Center titleView.lineBreakMode = NSLineBreakMode.ByTruncatingTail titleView.autoresizingMask = UIViewAutoresizing.FlexibleWidth - titleView.textColor = appStyle.navigationTitleColor + if peer.isPrivateSecret { + titleView.textColor = appStyle.navigationTitleSecretColor + } else { + titleView.textColor = appStyle.navigationTitleColor + } subtitleView.font = UIFont.systemFontOfSize(13) subtitleView.adjustsFontSizeToFitWidth = true @@ -234,21 +238,26 @@ public class ConversationViewController: } if (ActorSDK.sharedActor().enableCalls && !isBot && peer.isPrivate) { if ActorSDK.sharedActor().enableVideoCalls { - let callButtonView = AACallButton(image: UIImage.bundled("ic_call_outline_22")?.tintImage(ActorSDK.sharedActor().style.navigationTintColor)) + let callButtonView = AABarButton(image: UIImage.bundled("ic_call_outline_22")?.tintImage(ActorSDK.sharedActor().style.navigationTintColor)) callButtonView.viewDidTap = onCallTap let callButtonItem = UIBarButtonItem(customView: callButtonView) - let videoCallButtonView = AACallButton(image: UIImage.bundled("ic_video_outline_22")?.tintImage(ActorSDK.sharedActor().style.navigationTintColor)) + let videoCallButtonView = AABarButton(image: UIImage.bundled("ic_video_outline_22")?.tintImage(ActorSDK.sharedActor().style.navigationTintColor)) videoCallButtonView.viewDidTap = onVideoCallTap let callVideoButtonItem = UIBarButtonItem(customView: videoCallButtonView) self.navigationItem.rightBarButtonItems = [barItem, callVideoButtonItem, callButtonItem] } else { - let callButtonView = AACallButton(image: UIImage.bundled("ic_call_outline_22")?.tintImage(ActorSDK.sharedActor().style.navigationTintColor)) + let callButtonView = AABarButton(image: UIImage.bundled("ic_call_outline_22")?.tintImage(ActorSDK.sharedActor().style.navigationTintColor)) callButtonView.viewDidTap = onCallTap let callButtonItem = UIBarButtonItem(customView: callButtonView) self.navigationItem.rightBarButtonItems = [barItem, callButtonItem] } + } else if peer.isPrivateSecret { + let timerButtonView = AABarButton(image: UIImage.bundled("ic_timer_22")?.tintImage(ActorSDK.sharedActor().style.navigationTintColor)) + timerButtonView.viewDidTap = onTimerTap + let callButtonItem = UIBarButtonItem(customView: timerButtonView) + self.navigationItem.rightBarButtonItems = [barItem, callButtonItem] } else { self.navigationItem.rightBarButtonItems = [barItem] } @@ -300,21 +309,42 @@ public class ConversationViewController: super.viewWillAppear(animated) // Installing bindings - if (peer.peerType.ordinal() == ACPeerType.PRIVATE().ordinal()) { + if (peer.peerType == ACPeerType.PRIVATE() || peer.peerType == ACPeerType.PRIVATE_ENCRYPTED()) { let user = Actor.getUserWithUid(peer.peerId) let nameModel = user.getNameModel() - let blockStatus = user.isBlockedModel().get().booleanValue() + // let blockStatus = user.isBlockedModel().get().booleanValue() - binder.bind(nameModel, closure: { (value: NSString?) -> () in - self.titleView.text = String(value!) + binder.bind(nameModel, closure: { (value: String!) -> () in + + if self.peer.isPrivateSecret { + + let attachment = NSTextAttachment() + attachment.image = UIImage.bundled("ic_secret_title")? + .tintImage(self.appStyle.navigationTitleSecretColor) + attachment.bounds = CGRectMake(0, 0, 12, 12) + + let title = NSMutableAttributedString() + title.appendAttributedString(NSAttributedString(attachment: attachment)) + title.appendAttributedString(NSAttributedString(string: " \(value)")) + self.titleView.attributedText = title + } else { + self.titleView.text = value + } self.navigationView.sizeToFit() }) binder.bind(user.getAvatarModel(), closure: { (value: ACAvatar?) -> () in self.avatarView.bind(user.getNameModel().get(), id: Int(user.getId()), avatar: value) }) - binder.bind(Actor.getTypingWithUid(peer.peerId), valueModel2: user.getPresenceModel(), closure:{ (typing:JavaLangBoolean?, presence:ACUserPresence?) -> () in + + let typingModel: ARValueModel + if peer.peerType == ACPeerType.PRIVATE() { + typingModel = Actor.getTypingWithUid(peer.peerId) + } else { + typingModel = Actor.getSecretTypingWithUid(peer.peerId) + } + binder.bind(typingModel, valueModel2: user.getPresenceModel(), closure:{ (typing:JavaLangBoolean?, presence:ACUserPresence?) -> () in if (typing != nil && typing!.booleanValue()) { self.subtitleView.text = Actor.getFormatter().formatTyping() @@ -507,7 +537,7 @@ public class ConversationViewController: func onAvatarTap() { let id = Int(peer.peerId) var controller: AAViewController! - if (peer.peerType.ordinal() == ACPeerType.PRIVATE().ordinal()) { + if (peer.isPrivate || peer.isPrivateSecret) { controller = ActorSDK.sharedActor().delegate.actorControllerForUser(id) if controller == nil { controller = AAUserViewController(uid: id) @@ -548,6 +578,26 @@ public class ConversationViewController: } } + func onTimerTap() { + alertSheet { (a) in + a.action("disable", closure: { + self.executePromise(Actor.setSecretChatTimerWithUid(self.peer.peerId, withTimeout: nil)) + }) + a.action("1 sec", closure: { + self.executePromise(Actor.setSecretChatTimerWithUid(self.peer.peerId, withTimeout: JavaLangInteger(int: 1000))) + }) + a.action("2 sec", closure: { + self.executePromise(Actor.setSecretChatTimerWithUid(self.peer.peerId, withTimeout: JavaLangInteger(int: 2000))) + }) + a.action("3 sec", closure: { + self.executePromise(Actor.setSecretChatTimerWithUid(self.peer.peerId, withTimeout: JavaLangInteger(int: 3000))) + }) + a.action("1 minute", closure: { + self.executePromise(Actor.setSecretChatTimerWithUid(self.peer.peerId, withTimeout: JavaLangInteger(int: 60000))) + }) + } + } + //////////////////////////////////////////////////////////// // MARK: - Text bar actions //////////////////////////////////////////////////////////// @@ -1098,7 +1148,8 @@ class AABarAvatarView : AAAvatarView { } } -class AACallButton: UIImageView { +class AABarButton: UIImageView { + override init(image: UIImage?) { super.init(image: image) } diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/User/AAUserViewController.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/User/AAUserViewController.swift index d322040fd1..6a5252db67 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/User/AAUserViewController.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/User/AAUserViewController.swift @@ -64,8 +64,8 @@ class AAUserViewController: AAContentTableController { } } + // Profile: Starting Voice Call if (ActorSDK.sharedActor().enableCalls && !self.isBot) { - // Profile: Starting Voice Call s.action("CallsStartAudio") { (r) -> () in r.selectAction = { () -> Bool in self.execute(Actor.doCallWithUid(jint(self.uid))) @@ -77,15 +77,32 @@ class AAUserViewController: AAContentTableController { // Profile: Send messages s.action("ProfileSendMessage") { (r) -> () in r.selectAction = { () -> Bool in - if let customController = ActorSDK.sharedActor().delegate.actorControllerForConversation(ACPeer.userWithInt(jint(self.uid))) { + let peer = ACPeer.userWithInt(jint(self.uid)) + if let customController = ActorSDK.sharedActor().delegate.actorControllerForConversation(peer) { self.navigateDetail(customController) } else { - self.navigateDetail(ConversationViewController(peer: ACPeer.userWithInt(jint(self.uid)))) + self.navigateDetail(ConversationViewController(peer: peer)) } self.popover?.dismissPopoverAnimated(true) return false } } + + // Profile: Starting Secret Chat + if (ActorSDK.sharedActor().enableSecretChats && !self.isBot) { + s.action("ActionStartSecret") { (r) -> () in + r.selectAction = { () -> Bool in + let peer = ACPeer.secretWithInt(jint(self.uid)) + if let customController = ActorSDK.sharedActor().delegate.actorControllerForConversation(peer) { + self.navigateDetail(customController) + } else { + self.navigateDetail(ConversationViewController(peer: peer)) + } + self.popover?.dismissPopoverAnimated(true) + return false + } + } + } } let nick = self.user.getNickModel().get() diff --git a/actor-sdk/sdk-core-ios/VERSION b/actor-sdk/sdk-core-ios/VERSION index 415b19fc36..389f7740ee 100644 --- a/actor-sdk/sdk-core-ios/VERSION +++ b/actor-sdk/sdk-core-ios/VERSION @@ -1 +1 @@ -2.0 \ No newline at end of file +4.0 \ No newline at end of file diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/Messenger.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/Messenger.java index 75971c46af..2d06d99c0d 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/Messenger.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/Messenger.java @@ -67,6 +67,7 @@ import im.actor.core.viewmodel.UploadFileCallback; import im.actor.core.viewmodel.UploadFileVM; import im.actor.core.viewmodel.UploadFileVMCallback; +import im.actor.core.viewmodel.UserTypingVM; import im.actor.core.viewmodel.UserVM; import im.actor.runtime.actors.ActorSystem; import im.actor.runtime.actors.messages.Void; @@ -107,6 +108,7 @@ public Messenger(@NotNull Configuration configuration) { ActorSystem.system().setTraceInterface(new ActorTrace()); ActorSystem.system().addDispatcher("network_manager", 1); ActorSystem.system().addDispatcher("heavy", 2); + ActorSystem.system().addDispatcher("encrypt"); // Configure dispatcher // timing.section("Dispatcher"); @@ -507,7 +509,7 @@ public DialogGroupsVM getDialogGroupsVM() { } /** - * Get private chat ViewModel + * Get private chat typing ViewModel * * @param uid chat's User Id * @return ValueModel of Boolean for typing state @@ -518,6 +520,18 @@ public ValueModel getTyping(int uid) { return modules.getTypingModule().getTyping(uid).getTyping(); } + /** + * Get Secret chat typing View Model + * + * @param uid chat's User Id + * @return ValueModel of Boolean for typing state + */ + @NotNull + @ObjectiveCName("getSecretTypingWithUid:") + public ValueModel getSecretTyping(int uid) { + return modules.getTypingModule().getSecretTyping(uid).getTyping(); + } + /** * Get group chat ViewModel * @@ -1235,6 +1249,18 @@ public SearchValueModel buildGlobalSearchModel() { return modules.getSearchModule().buildSearchModel(); } + /** + * Setting secret chat timer + * + * @param uid user's id + * @param timeout user's timeout + * @return promice of void + */ + @ObjectiveCName("setSecretChatTimerWithUid:withTimeout:") + public Promise setSecretChatTimer(int uid, Integer timeout) { + return modules.getEncryption().setSecretChatTimer(uid, timeout); + } + ////////////////////////////////////// // Calls ////////////////////////////////////// @@ -1796,17 +1822,6 @@ public Command revokeIntegrationToken(int gid) { .failure(e -> callback.onError(e)); } - /** - * Check if chat with bot is started - * - * @param uid bot user id - * @return is chat with bot started - */ - @ObjectiveCName("isStartedWithUid:") - public Promise isStarted(int uid) { - return modules.getMessagesModule().chatIsEmpty(Peer.user(uid)); - } - ////////////////////////////////////// // Blocked List diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiDocumentEncryptionInfo.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiDocumentEncryptionInfo.java new file mode 100644 index 0000000000..9706b78415 --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiDocumentEncryptionInfo.java @@ -0,0 +1,77 @@ +package im.actor.core.api; +/* + * Generated by the Actor API Scheme generator. DO NOT EDIT! + */ + +import im.actor.runtime.bser.*; +import im.actor.runtime.collections.*; +import static im.actor.runtime.bser.Utils.*; +import im.actor.core.network.parser.*; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.NotNull; +import com.google.j2objc.annotations.ObjectiveCName; +import java.io.IOException; +import java.util.List; +import java.util.ArrayList; + +public class ApiDocumentEncryptionInfo extends BserObject { + + private int realFileSize; + private String keyAlg; + private byte[] key; + + public ApiDocumentEncryptionInfo(int realFileSize, @NotNull String keyAlg, @NotNull byte[] key) { + this.realFileSize = realFileSize; + this.keyAlg = keyAlg; + this.key = key; + } + + public ApiDocumentEncryptionInfo() { + + } + + public int getRealFileSize() { + return this.realFileSize; + } + + @NotNull + public String getKeyAlg() { + return this.keyAlg; + } + + @NotNull + public byte[] getKey() { + return this.key; + } + + @Override + public void parse(BserValues values) throws IOException { + this.realFileSize = values.getInt(1); + this.keyAlg = values.getString(2); + this.key = values.getBytes(3); + } + + @Override + public void serialize(BserWriter writer) throws IOException { + writer.writeInt(1, this.realFileSize); + if (this.keyAlg == null) { + throw new IOException(); + } + writer.writeString(2, this.keyAlg); + if (this.key == null) { + throw new IOException(); + } + writer.writeBytes(3, this.key); + } + + @Override + public String toString() { + String res = "struct DocumentEncryptionInfo{"; + res += "realFileSize=" + this.realFileSize; + res += ", keyAlg=" + this.keyAlg; + res += ", key=" + byteArrayToString(this.key); + res += "}"; + return res; + } + +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiDocumentMessage.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiDocumentMessage.java index dc58c0bd67..8c822f9cda 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiDocumentMessage.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiDocumentMessage.java @@ -23,8 +23,9 @@ public class ApiDocumentMessage extends ApiMessage { private String mimeType; private ApiFastThumb thumb; private ApiDocumentEx ext; + private ApiDocumentEncryptionInfo encryptionInfo; - public ApiDocumentMessage(long fileId, long accessHash, int fileSize, @NotNull String name, @NotNull String mimeType, @Nullable ApiFastThumb thumb, @Nullable ApiDocumentEx ext) { + public ApiDocumentMessage(long fileId, long accessHash, int fileSize, @NotNull String name, @NotNull String mimeType, @Nullable ApiFastThumb thumb, @Nullable ApiDocumentEx ext, @Nullable ApiDocumentEncryptionInfo encryptionInfo) { this.fileId = fileId; this.accessHash = accessHash; this.fileSize = fileSize; @@ -32,6 +33,7 @@ public ApiDocumentMessage(long fileId, long accessHash, int fileSize, @NotNull S this.mimeType = mimeType; this.thumb = thumb; this.ext = ext; + this.encryptionInfo = encryptionInfo; } public ApiDocumentMessage() { @@ -74,6 +76,11 @@ public ApiDocumentEx getExt() { return this.ext; } + @Nullable + public ApiDocumentEncryptionInfo getEncryptionInfo() { + return this.encryptionInfo; + } + @Override public void parse(BserValues values) throws IOException { this.fileId = values.getLong(1); @@ -85,6 +92,7 @@ public void parse(BserValues values) throws IOException { if (values.optBytes(8) != null) { this.ext = ApiDocumentEx.fromBytes(values.getBytes(8)); } + this.encryptionInfo = values.optObj(9, new ApiDocumentEncryptionInfo()); if (values.hasRemaining()) { setUnmappedObjects(values.buildRemaining()); } @@ -109,6 +117,9 @@ public void serialize(BserWriter writer) throws IOException { if (this.ext != null) { writer.writeBytes(8, this.ext.buildContainer()); } + if (this.encryptionInfo != null) { + writer.writeObject(9, this.encryptionInfo); + } if (this.getUnmappedObjects() != null) { SparseArray unmapped = this.getUnmappedObjects(); for (int i = 0; i < unmapped.size(); i++) { @@ -127,6 +138,7 @@ public String toString() { res += ", mimeType=" + this.mimeType; res += ", thumb=" + (this.thumb != null ? "set":"empty"); res += ", ext=" + (this.ext != null ? "set":"empty"); + res += ", encryptionInfo=" + this.encryptionInfo; res += "}"; return res; } diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiEncryptedChatTimerSet.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiEncryptedChatTimerSet.java new file mode 100644 index 0000000000..13777f6305 --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiEncryptedChatTimerSet.java @@ -0,0 +1,75 @@ +package im.actor.core.api; +/* + * Generated by the Actor API Scheme generator. DO NOT EDIT! + */ + +import im.actor.runtime.bser.*; +import im.actor.runtime.collections.*; +import static im.actor.runtime.bser.Utils.*; +import im.actor.core.network.parser.*; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.NotNull; +import com.google.j2objc.annotations.ObjectiveCName; +import java.io.IOException; +import java.util.List; +import java.util.ArrayList; + +public class ApiEncryptedChatTimerSet extends ApiEncryptedContent { + + private int receiverId; + private long rid; + private Integer timerMs; + + public ApiEncryptedChatTimerSet(int receiverId, long rid, @Nullable Integer timerMs) { + this.receiverId = receiverId; + this.rid = rid; + this.timerMs = timerMs; + } + + public ApiEncryptedChatTimerSet() { + + } + + public int getHeader() { + return 8; + } + + public int getReceiverId() { + return this.receiverId; + } + + public long getRid() { + return this.rid; + } + + @Nullable + public Integer getTimerMs() { + return this.timerMs; + } + + @Override + public void parse(BserValues values) throws IOException { + this.receiverId = values.getInt(1); + this.rid = values.getLong(3); + this.timerMs = values.optInt(2); + } + + @Override + public void serialize(BserWriter writer) throws IOException { + writer.writeInt(1, this.receiverId); + writer.writeLong(3, this.rid); + if (this.timerMs != null) { + writer.writeInt(2, this.timerMs); + } + } + + @Override + public String toString() { + String res = "struct EncryptedChatTimerSet{"; + res += "receiverId=" + this.receiverId; + res += ", timerMs=" + this.timerMs; + res += "}"; + return res; + } + +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiEncryptedContent.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiEncryptedContent.java new file mode 100644 index 0000000000..a48de5d25e --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiEncryptedContent.java @@ -0,0 +1,43 @@ +package im.actor.core.api; +/* + * Generated by the Actor API Scheme generator. DO NOT EDIT! + */ + +import im.actor.runtime.bser.*; +import im.actor.runtime.collections.*; +import static im.actor.runtime.bser.Utils.*; +import im.actor.core.network.parser.*; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.NotNull; +import com.google.j2objc.annotations.ObjectiveCName; +import java.io.IOException; +import java.util.List; +import java.util.ArrayList; + +public abstract class ApiEncryptedContent extends BserObject { + public static ApiEncryptedContent fromBytes(byte[] src) throws IOException { + BserValues values = new BserValues(BserParser.deserialize(new DataInput(src, 0, src.length))); + int key = values.getInt(1); + byte[] content = values.getBytes(2); + switch(key) { + case 1: return Bser.parse(new ApiEncryptedMessageContent(), content); + case 2: return Bser.parse(new ApiEncryptedEditContent(), content); + case 3: return Bser.parse(new ApiEncryptedDeleteContent(), content); + case 4: return Bser.parse(new ApiEncryptedReceived(), content); + case 5: return Bser.parse(new ApiEncryptedRead(), content); + case 6: return Bser.parse(new ApiEncryptedDeleteAll(), content); + case 8: return Bser.parse(new ApiEncryptedChatTimerSet(), content); + default: return new ApiEncryptedContentUnsupported(key, content); + } + } + public abstract int getHeader(); + + public byte[] buildContainer() throws IOException { + DataOutput res = new DataOutput(); + BserWriter writer = new BserWriter(res); + writer.writeInt(1, getHeader()); + writer.writeBytes(2, toByteArray()); + return res.toByteArray(); + } + +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiEncryptedContentUnsupported.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiEncryptedContentUnsupported.java new file mode 100644 index 0000000000..5c77f0f96e --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiEncryptedContentUnsupported.java @@ -0,0 +1,42 @@ +package im.actor.core.api; +/* + * Generated by the Actor API Scheme generator. DO NOT EDIT! + */ + +import im.actor.runtime.bser.*; +import im.actor.runtime.collections.*; +import static im.actor.runtime.bser.Utils.*; +import im.actor.core.network.parser.*; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.NotNull; +import com.google.j2objc.annotations.ObjectiveCName; +import java.io.IOException; +import java.util.List; +import java.util.ArrayList; + +public class ApiEncryptedContentUnsupported extends ApiEncryptedContent { + + private int key; + private byte[] content; + + public ApiEncryptedContentUnsupported(int key, byte[] content) { + this.key = key; + this.content = content; + } + + @Override + public int getHeader() { + return this.key; + } + + @Override + public void parse(BserValues values) throws IOException { + throw new IOException("Parsing is unsupported"); + } + + @Override + public void serialize(BserWriter writer) throws IOException { + writer.writeRaw(content); + } + +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiEncryptedData.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiEncryptedData.java new file mode 100644 index 0000000000..8582b87b42 --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiEncryptedData.java @@ -0,0 +1,64 @@ +package im.actor.core.api; +/* + * Generated by the Actor API Scheme generator. DO NOT EDIT! + */ + +import im.actor.runtime.bser.*; +import im.actor.runtime.collections.*; +import static im.actor.runtime.bser.Utils.*; +import im.actor.core.network.parser.*; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.NotNull; +import com.google.j2objc.annotations.ObjectiveCName; +import java.io.IOException; +import java.util.List; +import java.util.ArrayList; + +public class ApiEncryptedData extends BserObject { + + private int version; + private byte[] data; + + public ApiEncryptedData(int version, @NotNull byte[] data) { + this.version = version; + this.data = data; + } + + public ApiEncryptedData() { + + } + + public int getVersion() { + return this.version; + } + + @NotNull + public byte[] getData() { + return this.data; + } + + @Override + public void parse(BserValues values) throws IOException { + this.version = values.getInt(1); + this.data = values.getBytes(2); + } + + @Override + public void serialize(BserWriter writer) throws IOException { + writer.writeInt(1, this.version); + if (this.data == null) { + throw new IOException(); + } + writer.writeBytes(2, this.data); + } + + @Override + public String toString() { + String res = "struct EncryptedData{"; + res += "version=" + this.version; + res += ", data=" + byteArrayToString(this.data); + res += "}"; + return res; + } + +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiEncryptedDeleteAll.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiEncryptedDeleteAll.java new file mode 100644 index 0000000000..ecd28d5296 --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiEncryptedDeleteAll.java @@ -0,0 +1,55 @@ +package im.actor.core.api; +/* + * Generated by the Actor API Scheme generator. DO NOT EDIT! + */ + +import im.actor.runtime.bser.*; +import im.actor.runtime.collections.*; +import static im.actor.runtime.bser.Utils.*; +import im.actor.core.network.parser.*; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.NotNull; +import com.google.j2objc.annotations.ObjectiveCName; +import java.io.IOException; +import java.util.List; +import java.util.ArrayList; + +public class ApiEncryptedDeleteAll extends ApiEncryptedContent { + + private int receiverId; + + public ApiEncryptedDeleteAll(int receiverId) { + this.receiverId = receiverId; + } + + public ApiEncryptedDeleteAll() { + + } + + public int getHeader() { + return 6; + } + + public int getReceiverId() { + return this.receiverId; + } + + @Override + public void parse(BserValues values) throws IOException { + this.receiverId = values.getInt(1); + } + + @Override + public void serialize(BserWriter writer) throws IOException { + writer.writeInt(1, this.receiverId); + } + + @Override + public String toString() { + String res = "struct EncryptedDeleteAll{"; + res += "receiverId=" + this.receiverId; + res += "}"; + return res; + } + +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiEncryptedDeleteContent.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiEncryptedDeleteContent.java new file mode 100644 index 0000000000..5c9b36e82b --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiEncryptedDeleteContent.java @@ -0,0 +1,65 @@ +package im.actor.core.api; +/* + * Generated by the Actor API Scheme generator. DO NOT EDIT! + */ + +import im.actor.runtime.bser.*; +import im.actor.runtime.collections.*; +import static im.actor.runtime.bser.Utils.*; +import im.actor.core.network.parser.*; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.NotNull; +import com.google.j2objc.annotations.ObjectiveCName; +import java.io.IOException; +import java.util.List; +import java.util.ArrayList; + +public class ApiEncryptedDeleteContent extends ApiEncryptedContent { + + private int receiverId; + private List rid; + + public ApiEncryptedDeleteContent(int receiverId, @NotNull List rid) { + this.receiverId = receiverId; + this.rid = rid; + } + + public ApiEncryptedDeleteContent() { + + } + + public int getHeader() { + return 3; + } + + public int getReceiverId() { + return this.receiverId; + } + + @NotNull + public List getRid() { + return this.rid; + } + + @Override + public void parse(BserValues values) throws IOException { + this.receiverId = values.getInt(1); + this.rid = values.getRepeatedLong(2); + } + + @Override + public void serialize(BserWriter writer) throws IOException { + writer.writeInt(1, this.receiverId); + writer.writeRepeatedLong(2, this.rid); + } + + @Override + public String toString() { + String res = "struct EncryptedDeleteContent{"; + res += "receiverId=" + this.receiverId; + res += ", rid=" + this.rid; + res += "}"; + return res; + } + +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiEncryptedEditContent.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiEncryptedEditContent.java new file mode 100644 index 0000000000..e5ab3485f0 --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiEncryptedEditContent.java @@ -0,0 +1,78 @@ +package im.actor.core.api; +/* + * Generated by the Actor API Scheme generator. DO NOT EDIT! + */ + +import im.actor.runtime.bser.*; +import im.actor.runtime.collections.*; +import static im.actor.runtime.bser.Utils.*; +import im.actor.core.network.parser.*; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.NotNull; +import com.google.j2objc.annotations.ObjectiveCName; +import java.io.IOException; +import java.util.List; +import java.util.ArrayList; + +public class ApiEncryptedEditContent extends ApiEncryptedContent { + + private int receiverId; + private long rid; + private ApiMessage message; + + public ApiEncryptedEditContent(int receiverId, long rid, @NotNull ApiMessage message) { + this.receiverId = receiverId; + this.rid = rid; + this.message = message; + } + + public ApiEncryptedEditContent() { + + } + + public int getHeader() { + return 2; + } + + public int getReceiverId() { + return this.receiverId; + } + + public long getRid() { + return this.rid; + } + + @NotNull + public ApiMessage getMessage() { + return this.message; + } + + @Override + public void parse(BserValues values) throws IOException { + this.receiverId = values.getInt(1); + this.rid = values.getLong(2); + this.message = ApiMessage.fromBytes(values.getBytes(3)); + } + + @Override + public void serialize(BserWriter writer) throws IOException { + writer.writeInt(1, this.receiverId); + writer.writeLong(2, this.rid); + if (this.message == null) { + throw new IOException(); + } + + writer.writeBytes(3, this.message.buildContainer()); + } + + @Override + public String toString() { + String res = "struct EncryptedEditContent{"; + res += "receiverId=" + this.receiverId; + res += ", rid=" + this.rid; + res += ", message=" + this.message; + res += "}"; + return res; + } + +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiEncryptedGroup.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiEncryptedGroup.java new file mode 100644 index 0000000000..289445b30e --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiEncryptedGroup.java @@ -0,0 +1,74 @@ +package im.actor.core.api; +/* + * Generated by the Actor API Scheme generator. DO NOT EDIT! + */ + +import im.actor.runtime.bser.*; +import im.actor.runtime.collections.*; +import static im.actor.runtime.bser.Utils.*; +import im.actor.core.network.parser.*; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.NotNull; +import com.google.j2objc.annotations.ObjectiveCName; +import java.io.IOException; +import java.util.List; +import java.util.ArrayList; + +public class ApiEncryptedGroup extends BserObject { + + private long groupId; + private String title; + private List members; + + public ApiEncryptedGroup(long groupId, @NotNull String title, @NotNull List members) { + this.groupId = groupId; + this.title = title; + this.members = members; + } + + public ApiEncryptedGroup() { + + } + + public long getGroupId() { + return this.groupId; + } + + @NotNull + public String getTitle() { + return this.title; + } + + @NotNull + public List getMembers() { + return this.members; + } + + @Override + public void parse(BserValues values) throws IOException { + this.groupId = values.getLong(1); + this.title = values.getString(2); + this.members = values.getRepeatedInt(3); + } + + @Override + public void serialize(BserWriter writer) throws IOException { + writer.writeLong(1, this.groupId); + if (this.title == null) { + throw new IOException(); + } + writer.writeString(2, this.title); + writer.writeRepeatedInt(3, this.members); + } + + @Override + public String toString() { + String res = "struct EncryptedGroup{"; + res += "groupId=" + this.groupId; + res += ", title=" + this.title; + res += ", members=" + this.members.size(); + res += "}"; + return res; + } + +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiEncryptedMessageContent.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiEncryptedMessageContent.java new file mode 100644 index 0000000000..208501e051 --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiEncryptedMessageContent.java @@ -0,0 +1,90 @@ +package im.actor.core.api; +/* + * Generated by the Actor API Scheme generator. DO NOT EDIT! + */ + +import im.actor.runtime.bser.*; +import im.actor.runtime.collections.*; +import static im.actor.runtime.bser.Utils.*; +import im.actor.core.network.parser.*; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.NotNull; +import com.google.j2objc.annotations.ObjectiveCName; +import java.io.IOException; +import java.util.List; +import java.util.ArrayList; + +public class ApiEncryptedMessageContent extends ApiEncryptedContent { + + private int receiverId; + private long rid; + private ApiMessage message; + private Integer timerMs; + + public ApiEncryptedMessageContent(int receiverId, long rid, @NotNull ApiMessage message, @Nullable Integer timerMs) { + this.receiverId = receiverId; + this.rid = rid; + this.message = message; + this.timerMs = timerMs; + } + + public ApiEncryptedMessageContent() { + + } + + public int getHeader() { + return 1; + } + + public int getReceiverId() { + return this.receiverId; + } + + public long getRid() { + return this.rid; + } + + @NotNull + public ApiMessage getMessage() { + return this.message; + } + + @Nullable + public Integer getTimerMs() { + return this.timerMs; + } + + @Override + public void parse(BserValues values) throws IOException { + this.receiverId = values.getInt(1); + this.rid = values.getLong(2); + this.message = ApiMessage.fromBytes(values.getBytes(3)); + this.timerMs = values.optInt(4); + } + + @Override + public void serialize(BserWriter writer) throws IOException { + writer.writeInt(1, this.receiverId); + writer.writeLong(2, this.rid); + if (this.message == null) { + throw new IOException(); + } + + writer.writeBytes(3, this.message.buildContainer()); + if (this.timerMs != null) { + writer.writeInt(4, this.timerMs); + } + } + + @Override + public String toString() { + String res = "struct EncryptedMessageContent{"; + res += "receiverId=" + this.receiverId; + res += ", rid=" + this.rid; + res += ", message=" + this.message; + res += ", timerMs=" + this.timerMs; + res += "}"; + return res; + } + +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiEncryptedRead.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiEncryptedRead.java new file mode 100644 index 0000000000..1579c294c2 --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiEncryptedRead.java @@ -0,0 +1,64 @@ +package im.actor.core.api; +/* + * Generated by the Actor API Scheme generator. DO NOT EDIT! + */ + +import im.actor.runtime.bser.*; +import im.actor.runtime.collections.*; +import static im.actor.runtime.bser.Utils.*; +import im.actor.core.network.parser.*; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.NotNull; +import com.google.j2objc.annotations.ObjectiveCName; +import java.io.IOException; +import java.util.List; +import java.util.ArrayList; + +public class ApiEncryptedRead extends ApiEncryptedContent { + + private int receiverId; + private long readDate; + + public ApiEncryptedRead(int receiverId, long readDate) { + this.receiverId = receiverId; + this.readDate = readDate; + } + + public ApiEncryptedRead() { + + } + + public int getHeader() { + return 5; + } + + public int getReceiverId() { + return this.receiverId; + } + + public long getReadDate() { + return this.readDate; + } + + @Override + public void parse(BserValues values) throws IOException { + this.receiverId = values.getInt(1); + this.readDate = values.getLong(2); + } + + @Override + public void serialize(BserWriter writer) throws IOException { + writer.writeInt(1, this.receiverId); + writer.writeLong(2, this.readDate); + } + + @Override + public String toString() { + String res = "struct EncryptedRead{"; + res += "receiverId=" + this.receiverId; + res += ", readDate=" + this.readDate; + res += "}"; + return res; + } + +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiEncryptedReceived.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiEncryptedReceived.java new file mode 100644 index 0000000000..bbbf408bf2 --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiEncryptedReceived.java @@ -0,0 +1,64 @@ +package im.actor.core.api; +/* + * Generated by the Actor API Scheme generator. DO NOT EDIT! + */ + +import im.actor.runtime.bser.*; +import im.actor.runtime.collections.*; +import static im.actor.runtime.bser.Utils.*; +import im.actor.core.network.parser.*; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.NotNull; +import com.google.j2objc.annotations.ObjectiveCName; +import java.io.IOException; +import java.util.List; +import java.util.ArrayList; + +public class ApiEncryptedReceived extends ApiEncryptedContent { + + private int receiverId; + private long receiveDate; + + public ApiEncryptedReceived(int receiverId, long receiveDate) { + this.receiverId = receiverId; + this.receiveDate = receiveDate; + } + + public ApiEncryptedReceived() { + + } + + public int getHeader() { + return 4; + } + + public int getReceiverId() { + return this.receiverId; + } + + public long getReceiveDate() { + return this.receiveDate; + } + + @Override + public void parse(BserValues values) throws IOException { + this.receiverId = values.getInt(1); + this.receiveDate = values.getLong(2); + } + + @Override + public void serialize(BserWriter writer) throws IOException { + writer.writeInt(1, this.receiverId); + writer.writeLong(2, this.receiveDate); + } + + @Override + public String toString() { + String res = "struct EncryptedReceived{"; + res += "receiverId=" + this.receiverId; + res += ", receiveDate=" + this.receiveDate; + res += "}"; + return res; + } + +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiMessage.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiMessage.java index 4782926df9..32720606e1 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiMessage.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiMessage.java @@ -27,7 +27,6 @@ public static ApiMessage fromBytes(byte[] src) throws IOException { case 5: return Bser.parse(new ApiUnsupportedMessage(), content); case 6: return Bser.parse(new ApiStickerMessage(), content); case 7: return Bser.parse(new ApiBinaryMessage(), content); - case 8: return Bser.parse(new ApiEncryptedMessage(), content); case 9: return Bser.parse(new ApiEmptyMessage(), content); default: return new ApiMessageUnsupported(key, content); } diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiServiceEx.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiServiceEx.java index a970fe563d..3e7ebee995 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiServiceEx.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiServiceEx.java @@ -34,6 +34,7 @@ public static ApiServiceEx fromBytes(byte[] src) throws IOException { case 16: return Bser.parse(new ApiServiceExPhoneCall(), content); case 20: return Bser.parse(new ApiServiceExChatArchived(), content); case 21: return Bser.parse(new ApiServiceExChatRestored(), content); + case 23: return Bser.parse(new ApiServiceTimerChanged(), content); default: return new ApiServiceExUnsupported(key, content); } } diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiEncryptedMessage.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiServiceTimerChanged.java similarity index 68% rename from actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiEncryptedMessage.java rename to actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiServiceTimerChanged.java index 84ea016c48..719f637d90 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiEncryptedMessage.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiServiceTimerChanged.java @@ -14,30 +14,29 @@ import java.util.List; import java.util.ArrayList; -public class ApiEncryptedMessage extends ApiMessage { +public class ApiServiceTimerChanged extends ApiServiceEx { - private ApiEncryptedBox box; + private int timerMs; - public ApiEncryptedMessage(@NotNull ApiEncryptedBox box) { - this.box = box; + public ApiServiceTimerChanged(int timerMs) { + this.timerMs = timerMs; } - public ApiEncryptedMessage() { + public ApiServiceTimerChanged() { } public int getHeader() { - return 8; + return 23; } - @NotNull - public ApiEncryptedBox getBox() { - return this.box; + public int getTimerMs() { + return this.timerMs; } @Override public void parse(BserValues values) throws IOException { - this.box = values.getObj(1, new ApiEncryptedBox()); + this.timerMs = values.getInt(1); if (values.hasRemaining()) { setUnmappedObjects(values.buildRemaining()); } @@ -45,10 +44,7 @@ public void parse(BserValues values) throws IOException { @Override public void serialize(BserWriter writer) throws IOException { - if (this.box == null) { - throw new IOException(); - } - writer.writeObject(1, this.box); + writer.writeInt(1, this.timerMs); if (this.getUnmappedObjects() != null) { SparseArray unmapped = this.getUnmappedObjects(); for (int i = 0; i < unmapped.size(); i++) { @@ -60,8 +56,8 @@ public void serialize(BserWriter writer) throws IOException { @Override public String toString() { - String res = "struct EncryptedMessage{"; - res += "box=" + this.box; + String res = "struct ServiceTimerChanged{"; + res += "timerMs=" + this.timerMs; res += "}"; return res; } diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/AvatarImage.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/AvatarImage.java index 53f11b1183..45c3cc58f6 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/AvatarImage.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/AvatarImage.java @@ -80,7 +80,7 @@ protected void applyWrapped(@NotNull ApiAvatarImage wrapped) { this.width = wrapped.getWidth(); this.height = wrapped.getHeight(); this.fileReference = new FileReference(wrapped.getFileLocation(), - "avatar.jpg", wrapped.getFileSize()); + "avatar.jpg", wrapped.getFileSize(), null); } @Override diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/ContentDescription.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/ContentDescription.java index 9452bee01f..7326609aeb 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/ContentDescription.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/ContentDescription.java @@ -26,6 +26,7 @@ import im.actor.core.entity.content.ServiceGroupUserJoined; import im.actor.core.entity.content.ServiceGroupUserKicked; import im.actor.core.entity.content.ServiceGroupUserLeave; +import im.actor.core.entity.content.ServiceTimerChanged; import im.actor.core.entity.content.ServiceUserRegistered; import im.actor.core.entity.content.StickerContent; import im.actor.core.entity.content.TextContent; @@ -96,6 +97,13 @@ public static ContentDescription fromContent(AbsContent msg) { } else if (msg instanceof ServiceGroupUserJoined) { return new ContentDescription(ContentType.SERVICE_JOINED, "", 0, false); + } else if (msg instanceof ServiceTimerChanged) { + ServiceTimerChanged timerChanged = (ServiceTimerChanged) msg; + if (timerChanged.getTimer() > 0) { + return new ContentDescription(ContentType.SERVICE_TIMER_SET, "", 0, false); + } else { + return new ContentDescription(ContentType.SERVICE_TIMER_CLEAR, "", 0, false); + } } else if (msg instanceof ServiceContent) { return new ContentDescription(ContentType.SERVICE, ((ServiceContent) msg).getCompatText(), 0, false); diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/ContentType.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/ContentType.java index 9320fe4403..be090f428b 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/ContentType.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/ContentType.java @@ -28,6 +28,8 @@ public enum ContentType { SERVICE_CALL_MISSED(23), SERVICE_TOPIC(24), SERVICE_ABOUT(25), + SERVICE_TIMER_SET(28), + SERVICE_TIMER_CLEAR(29), UNKNOWN_CONTENT(15); int value; @@ -91,7 +93,10 @@ public static ContentType fromValue(int value) { return SERVICE_TOPIC; case 25: return SERVICE_ABOUT; - + case 28: + return SERVICE_TIMER_SET; + case 29: + return SERVICE_TIMER_CLEAR; } } } diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/EncryptedConversationState.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/EncryptedConversationState.java new file mode 100644 index 0000000000..2d6084c47e --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/EncryptedConversationState.java @@ -0,0 +1,172 @@ +package im.actor.core.entity; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import im.actor.runtime.bser.Bser; +import im.actor.runtime.bser.BserCreator; +import im.actor.runtime.bser.BserObject; +import im.actor.runtime.bser.BserValues; +import im.actor.runtime.bser.BserWriter; +import im.actor.runtime.mvvm.ValueDefaultCreator; +import im.actor.runtime.storage.KeyValueItem; + +public class EncryptedConversationState extends BserObject implements KeyValueItem { + + public static EncryptedConversationState fromBytes(byte[] data) throws IOException { + return Bser.parse(new EncryptedConversationState(), data); + } + + public static BserCreator CREATOR = EncryptedConversationState::new; + + public static ValueDefaultCreator DEFAULT_CREATOR = id -> + new EncryptedConversationState((int) id, false, 0, 0, new ArrayList<>()); + + private int uid; + private boolean isLoaded; + private int timer; + private long timerDate; + private ArrayList unreadMessages; + + + public EncryptedConversationState(int uid, boolean isLoaded, int timer, long timerDate, ArrayList shortMessages) { + this.uid = uid; + this.isLoaded = isLoaded; + this.timer = timer; + this.timerDate = timerDate; + this.unreadMessages = shortMessages; + } + + private EncryptedConversationState() { + } + + public int getUid() { + return uid; + } + + public boolean isLoaded() { + return isLoaded; + } + + public int getTimer() { + return timer; + } + + public int getUnreadCount() { + return unreadMessages.size(); + } + + public long getTimerDate() { + return timerDate; + } + + public EncryptedConversationState editTimer(int timer, long timerDate) { + return new EncryptedConversationState(uid, isLoaded, timer, timerDate, unreadMessages); + } + + public EncryptedConversationState addUnreadMessages(List messages) { + ArrayList res = new ArrayList<>(); + res.addAll(this.unreadMessages); + res.addAll(messages); + return new EncryptedConversationState(uid, isLoaded, timer, timerDate, res); + } + + public EncryptedConversationState readBefore(long sortDate) { + ArrayList res = new ArrayList<>(unreadMessages); + ShortMessage m; + for (int i = res.size() - 1; i >= 0; i--) { + m = res.get(i); + if (m.getSortDate() <= sortDate) { + res.remove(m); + } + } + return new EncryptedConversationState(uid, isLoaded, timer, timerDate, res); + } + + public EncryptedConversationState read(List ridsToRead) { + ArrayList res = new ArrayList<>(unreadMessages); + ArrayList rem = new ArrayList<>(); + for (Long rid : ridsToRead) { + rem.add(new ShortMessage(0, rid)); + } + res.removeAll(rem); + return new EncryptedConversationState(uid, isLoaded, timer, timerDate, res); + } + + public EncryptedConversationState readAll() { + ArrayList res = new ArrayList<>(); + return new EncryptedConversationState(uid, isLoaded, timer, timerDate, res); + } + + @Override + public void parse(BserValues values) throws IOException { + uid = values.getInt(1); + isLoaded = values.getBool(2); + timer = values.getInt(3); + timerDate = values.getLong(4); + unreadMessages = new ArrayList<>(); + for (byte[] i : values.getRepeatedBytes(5)) { + ShortMessage m = new ShortMessage(); + Bser.parse(m, i); + unreadMessages.add(m); + } + } + + @Override + public void serialize(BserWriter writer) throws IOException { + writer.writeInt(1, uid); + writer.writeBool(2, isLoaded); + writer.writeLong(3, timer); + writer.writeLong(4, timerDate); + writer.writeRepeatedObj(5, unreadMessages); + } + + @Override + public long getEngineId() { + return uid; + } + + public static class ShortMessage extends BserObject { + private long sortDate; + private long rid; + + public ShortMessage() { + } + + public ShortMessage(long sortDate, long rid) { + this.sortDate = sortDate; + this.rid = rid; + } + + public long getSortDate() { + return sortDate; + } + + public long getRid() { + return rid; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ShortMessage that = (ShortMessage) o; + + return sortDate == that.sortDate || rid == that.rid; + } + + @Override + public void parse(BserValues values) throws IOException { + sortDate = values.getLong(1); + rid = values.getLong(2); + } + + @Override + public void serialize(BserWriter writer) throws IOException { + writer.writeLong(1, sortDate); + writer.writeLong(2, rid); + } + } +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/FileReference.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/FileReference.java index 6454a67cb7..4a600df19c 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/FileReference.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/FileReference.java @@ -10,7 +10,9 @@ import java.io.IOException; +import im.actor.core.api.ApiDocumentEncryptionInfo; import im.actor.core.api.ApiFileLocation; +import im.actor.runtime.bser.Bser; import im.actor.runtime.bser.BserValues; import im.actor.runtime.bser.BserWriter; @@ -28,11 +30,15 @@ public class FileReference extends WrapperEntity { private int fileSize; @Property("readonly, nonatomic") private String fileName; + @Property("readonly, nonatomic") + private ApiDocumentEncryptionInfo encryptionInfo; - public FileReference(ApiFileLocation fileLocation, String fileName, int fileSize) { + public FileReference(ApiFileLocation fileLocation, String fileName, int fileSize, + ApiDocumentEncryptionInfo encryptionInfo) { super(RECORD_ID, fileLocation); this.fileSize = fileSize; this.fileName = fileName; + this.encryptionInfo = encryptionInfo; } public FileReference(byte[] data) throws IOException { @@ -59,6 +65,10 @@ public String getFileName() { return fileName; } + public ApiDocumentEncryptionInfo getEncryptionInfo() { + return encryptionInfo; + } + @Override public void parse(BserValues values) throws IOException { // Is Old layout @@ -73,6 +83,11 @@ public void parse(BserValues values) throws IOException { fileSize = values.getInt(3); fileName = values.getString(4); + + byte[] data = values.optBytes(6); + if (data != null) { + encryptionInfo = Bser.parse(new ApiDocumentEncryptionInfo(), data); + } } @Override @@ -82,6 +97,10 @@ public void serialize(BserWriter writer) throws IOException { writer.writeInt(3, fileSize); writer.writeString(4, fileName); + if (encryptionInfo != null) { + writer.writeObject(6, encryptionInfo); + } + // Write wrapper super.serialize(writer); } diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/ImageLocation.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/ImageLocation.java index 9a6c1c75fa..332a609c3a 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/ImageLocation.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/ImageLocation.java @@ -22,7 +22,8 @@ public ImageLocation(@NotNull ApiImageLocation imageLocation, @NotNull String fi reference = new FileReference( imageLocation.getFileLocation(), fileName, - imageLocation.getFileSize()); + imageLocation.getFileSize(), + null); } public int getWidth() { diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/Message.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/Message.java index 37c511e242..6a6e4b5c78 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/Message.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/Message.java @@ -24,12 +24,7 @@ public static Message fromBytes(byte[] data) throws IOException { return Bser.parse(new Message(), data); } - public static final BserCreator CREATOR = new BserCreator() { - @Override - public Message createInstance() { - return new Message(); - } - }; + public static final BserCreator CREATOR = Message::new; public static final String ENTITY_NAME = "Message"; @@ -49,13 +44,15 @@ public Message createInstance() { private List reactions; @Property("readonly, nonatomic") private int contentIndex; + @Property("readonly, nonatomic") + private int timer; public Message(long rid, long sortDate, long date, int senderId, MessageState messageState, AbsContent content) { - this(rid, sortDate, date, senderId, messageState, content, new ArrayList(), 0); + this(rid, sortDate, date, senderId, messageState, content, new ArrayList<>(), 0, 0); } public Message(long rid, long sortDate, long date, int senderId, MessageState messageState, AbsContent content, - List reactions, int contentIndex) { + List reactions, int contentIndex, int timer) { this.rid = rid; this.sortDate = sortDate; this.date = date; @@ -64,6 +61,7 @@ public Message(long rid, long sortDate, long date, int senderId, MessageState me this.content = content; this.reactions = reactions; this.contentIndex = contentIndex; + this.timer = timer; } protected Message() { @@ -114,24 +112,28 @@ public AbsContent getContent() { return content; } + public int getTimer() { + return timer; + } + public Message changeState(MessageState messageState) { - return new Message(rid, sortDate, date, senderId, messageState, content, reactions, contentIndex); + return new Message(rid, sortDate, date, senderId, messageState, content, reactions, contentIndex, timer); } public Message changeDate(long date) { - return new Message(rid, sortDate, date, senderId, messageState, content, reactions, contentIndex); + return new Message(rid, sortDate, date, senderId, messageState, content, reactions, contentIndex, timer); } public Message changeAllDate(long date) { - return new Message(rid, date, date, senderId, messageState, content, reactions, contentIndex); + return new Message(rid, date, date, senderId, messageState, content, reactions, contentIndex, timer); } public Message changeContent(AbsContent content) { - return new Message(rid, sortDate, date, senderId, messageState, content, reactions, contentIndex + 1); + return new Message(rid, sortDate, date, senderId, messageState, content, reactions, contentIndex + 1, timer); } public Message changeReactions(List reactions) { - return new Message(rid, sortDate, date, senderId, messageState, content, reactions, contentIndex); + return new Message(rid, sortDate, date, senderId, messageState, content, reactions, contentIndex, timer); } @Override @@ -147,6 +149,7 @@ public void parse(BserValues values) throws IOException { reactions.add(Reaction.fromBytes(react)); } contentIndex = values.getInt(8, 0); + timer = values.getInt(9, 0); } @Override @@ -159,6 +162,7 @@ public void serialize(BserWriter writer) throws IOException { writer.writeBytes(6, AbsContent.serialize(content)); writer.writeRepeatedObj(7, reactions); writer.writeInt(8, contentIndex); + writer.writeInt(9, timer); } @Override diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/Peer.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/Peer.java index ee160ced49..4f5016a7ad 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/Peer.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/Peer.java @@ -16,12 +16,7 @@ public class Peer extends BserObject { - public static final BserCreator CREATOR = new BserCreator() { - @Override - public Peer createInstance() { - return new Peer(); - } - }; + public static final BserCreator CREATOR = Peer::new; public static Peer fromBytes(byte[] data) throws IOException { return Bser.parse(new Peer(), data); @@ -37,6 +32,8 @@ public static Peer fromUniqueId(long uid) { return new Peer(PeerType.PRIVATE, id); case 1: return new Peer(PeerType.GROUP, id); + case 2: + return new Peer(PeerType.PRIVATE_ENCRYPTED, id); } } @@ -48,6 +45,10 @@ public static Peer group(int gid) { return new Peer(PeerType.GROUP, gid); } + public static Peer secret(int uid) { + return new Peer(PeerType.PRIVATE_ENCRYPTED, uid); + } + @Property("readonly, nonatomic") private PeerType peerType; @Property("readonly, nonatomic") @@ -72,6 +73,9 @@ public long getUnuqueId() { case GROUP: type = 1; break; + case PRIVATE_ENCRYPTED: + type = 2; + break; } return ((long) peerId & 0xFFFFFFFFL) + (((long) type & 0xFFFFFFFFL) << 32); } @@ -146,4 +150,11 @@ public String toString() { public String toIdString() { return peerType + "_" + peerId; } + + public Peer toUnencryptedCompat() { + if (peerType == PeerType.PRIVATE_ENCRYPTED) { + return new Peer(PeerType.PRIVATE, peerId); + } + return this; + } } diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/Sticker.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/Sticker.java index 5d7d0f0ad9..6f945b148c 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/Sticker.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/Sticker.java @@ -67,14 +67,14 @@ protected void applyWrapped(@NotNull ApiStickerDescriptor wrapped) { emoji = wrapped.getEmoji(); id = wrapped.getId(); image128Location = wrapped.getImage128(); - image128 = new FileReference(image128Location.getFileLocation(), "sticker.webp", image128Location.getFileSize()); + image128 = new FileReference(image128Location.getFileLocation(), "sticker.webp", image128Location.getFileSize(), null); if (wrapped.getImage256() != null) { image256Location = wrapped.getImage256(); - image256 = new FileReference(image256Location.getFileLocation(), "sticker.webp", image256Location.getFileSize()); + image256 = new FileReference(image256Location.getFileLocation(), "sticker.webp", image256Location.getFileSize(), null); } if (wrapped.getImage512() != null) { image512Location = wrapped.getImage512(); - image512 = new FileReference(wrapped.getImage512().getFileLocation(), "sticker.webp", wrapped.getImage512().getFileSize()); + image512 = new FileReference(wrapped.getImage512().getFileLocation(), "sticker.webp", wrapped.getImage512().getFileSize(), null); } } diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/User.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/User.java index 2e2462fab4..965ec11bf9 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/User.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/User.java @@ -101,6 +101,11 @@ public Peer peer() { return new Peer(PeerType.PRIVATE, uid); } + @NotNull + public Peer secretPeer() { + return new Peer(PeerType.PRIVATE_ENCRYPTED, uid); + } + public int getUid() { return uid; } diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/content/AbsContent.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/content/AbsContent.java index a746ffd5c7..fb84942db1 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/content/AbsContent.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/content/AbsContent.java @@ -27,6 +27,7 @@ import im.actor.core.api.ApiServiceExUserKicked; import im.actor.core.api.ApiServiceExUserLeft; import im.actor.core.api.ApiServiceMessage; +import im.actor.core.api.ApiServiceTimerChanged; import im.actor.core.api.ApiStickerMessage; import im.actor.core.api.ApiTextMessage; import im.actor.core.entity.content.internal.AbsContentContainer; @@ -144,6 +145,8 @@ protected static AbsContent convertData(AbsContentContainer container) { return new ServiceCallEnded(remoteContainer); } else if (ext instanceof ApiServiceExPhoneMissed) { return new ServiceCallMissed(remoteContainer); + } else if (ext instanceof ApiServiceTimerChanged) { + return new ServiceTimerChanged(remoteContainer); } else { return new ServiceContent(remoteContainer); } diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/content/AnimationContent.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/content/AnimationContent.java index 2e5639957f..e0f7d5124e 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/content/AnimationContent.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/content/AnimationContent.java @@ -38,7 +38,8 @@ public static AnimationContent createRemoteAnimation(FileReference reference, in fastThumb.getH(), fastThumb.getImage()) : null, - new ApiDocumentExAnimation(w, h)))); + new ApiDocumentExAnimation(w, h), + reference.getEncryptionInfo()))); } private int w; diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/content/DocumentContent.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/content/DocumentContent.java index 22d83e7de5..be49db2ecf 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/content/DocumentContent.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/content/DocumentContent.java @@ -29,13 +29,9 @@ public static DocumentContent createRemoteDocument(FileReference reference, Fast reference.getFileSize(), reference.getFileName(), "image/jpeg", - fastThumb != null ? - new ApiFastThumb( - fastThumb.getW(), - fastThumb.getH(), - fastThumb.getImage()) : - null, - null))); + fastThumb != null ? new ApiFastThumb(fastThumb.getW(), fastThumb.getH(), fastThumb.getImage()) : null, + null, + reference.getEncryptionInfo()))); } protected FileSource source; @@ -48,7 +44,8 @@ public DocumentContent(ContentRemoteContainer contentContainer) { ApiDocumentMessage doc = ((ApiDocumentMessage) contentContainer.getMessage()); source = new FileRemoteSource(new FileReference( new ApiFileLocation(doc.getFileId(), doc.getAccessHash()), doc.getName(), - doc.getFileSize())); + doc.getFileSize(), + doc.getEncryptionInfo())); mimeType = doc.getMimeType(); name = doc.getName(); fastThumb = doc.getThumb() != null ? new FastThumb(doc.getThumb()) : null; diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/content/PhotoContent.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/content/PhotoContent.java index ba569acc38..d16d17e8e2 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/content/PhotoContent.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/content/PhotoContent.java @@ -46,7 +46,8 @@ public static PhotoContent createRemotePhoto(@NotNull FileReference reference, i fastThumb.getH(), fastThumb.getImage()) : null, - new ApiDocumentExPhoto(w, h)))); + new ApiDocumentExPhoto(w, h), + reference.getEncryptionInfo()))); } private int w; diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/content/ServiceTimerChanged.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/content/ServiceTimerChanged.java new file mode 100644 index 0000000000..bd84ef7988 --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/content/ServiceTimerChanged.java @@ -0,0 +1,26 @@ +package im.actor.core.entity.content; + +import im.actor.core.api.ApiServiceMessage; +import im.actor.core.api.ApiServiceTimerChanged; +import im.actor.core.entity.content.internal.ContentRemoteContainer; + +public class ServiceTimerChanged extends ServiceContent { + + public static ServiceTimerChanged create(int timer) { + return new ServiceTimerChanged(new ContentRemoteContainer( + new ApiServiceMessage("Timer changed", new ApiServiceTimerChanged(timer)))); + } + + private int timer; + + public ServiceTimerChanged(ContentRemoteContainer contentContainer) { + super(contentContainer); + + ApiServiceMessage serviceMessage = (ApiServiceMessage) contentContainer.getMessage(); + timer = ((ApiServiceTimerChanged) serviceMessage.getExt()).getTimerMs(); + } + + public int getTimer() { + return timer; + } +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/content/VideoContent.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/content/VideoContent.java index 0acbf23608..0aafc41446 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/content/VideoContent.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/content/VideoContent.java @@ -41,7 +41,8 @@ public static VideoContent createRemoteVideo(FileReference reference, int w, int fastThumb.getH(), fastThumb.getImage()) : null, - new ApiDocumentExVideo(w, h, duration)))); + new ApiDocumentExVideo(w, h, duration), + reference.getEncryptionInfo()))); } private int duration; diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/content/VoiceContent.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/content/VoiceContent.java index 7aa2ce3a88..93568db5aa 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/content/VoiceContent.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/content/VoiceContent.java @@ -41,7 +41,8 @@ public static VoiceContent createRemoteAudio(@NotNull FileReference reference, i reference.getFileName(), "audio/mp3", null, - new ApiDocumentExVoice(duration)))); + new ApiDocumentExVoice(duration), + reference.getEncryptionInfo()))); } private int duration; diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/encryption/PeerSession.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/encryption/PeerSession.java index 79291bee86..8b8d0cb606 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/encryption/PeerSession.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/encryption/PeerSession.java @@ -12,31 +12,17 @@ public class PeerSession extends BserObject { public static Predicate BY_THEIR_GROUP(final int theirKeyGroupId) { - return new Predicate() { - @Override - public boolean apply(PeerSession session) { - return session.getTheirKeyGroupId() == theirKeyGroupId; - } - }; + return session -> session.getTheirKeyGroupId() == theirKeyGroupId; } public static Predicate BY_IDS(final int theirKeyGroupId, final long ownPreKeyId, final long theirPreKeyId) { - return new Predicate() { - @Override - public boolean apply(PeerSession session) { - return session.getTheirKeyGroupId() == theirKeyGroupId && - session.getOwnPreKeyId() == ownPreKeyId && - session.getTheirPreKeyId() == theirPreKeyId; - } - }; + return session -> session.getTheirKeyGroupId() == theirKeyGroupId && + session.getOwnPreKeyId() == ownPreKeyId && + session.getTheirPreKeyId() == theirPreKeyId; } - public static final Comparator COMPARATOR = new Comparator() { - @Override - public int compare(PeerSession lhs, PeerSession rhs) { - return ByteStrings.compare(lhs.getMasterKey(), rhs.getMasterKey()); - } - }; + public static final Comparator COMPARATOR = (lhs, rhs) -> + ByteStrings.compare(lhs.getMasterKey(), rhs.getMasterKey()); private long sid; private int uid; diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/i18n/I18nEngine.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/i18n/I18nEngine.java index 1b4831bafe..c2ff61be00 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/i18n/I18nEngine.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/i18n/I18nEngine.java @@ -30,6 +30,7 @@ import im.actor.core.entity.content.ServiceGroupUserJoined; import im.actor.core.entity.content.ServiceGroupUserKicked; import im.actor.core.entity.content.ServiceGroupUserLeave; +import im.actor.core.entity.content.ServiceTimerChanged; import im.actor.core.entity.content.ServiceUserRegistered; import im.actor.core.entity.content.TextContent; import im.actor.core.modules.Modules; @@ -125,6 +126,29 @@ public String formatTyping(int count) { .replace("{count}", "" + count); } + // + // Duration + // + + @ObjectiveCName("formatDurationWithMsec:") + public String formatDuration(int msec) { + if (msec < 1000) { + return getPlural("language.format.time.seconds.full", 1) + .replace("{seconds}", "1"); + } else if (msec < 60000) { + int secs = (msec / 1000); + return getPlural("language.format.time.seconds.full", secs) + .replace("{seconds}", "" + secs); + } else if (msec < 24 * (60 * 60000)) { + int minutes = msec / 60000; + return getPlural("language.format.time.minutes.full", minutes) + .replace("{minutes}", "" + minutes); + } else { + int hours = msec / (60 * 60000); + return getPlural("language.format.time.hours.full", hours) + .replace("{hours}", "" + hours); + } + } // // Presence @@ -354,6 +378,10 @@ public String formatContentText(int senderId, ContentType contentType, String te return get("content.service.calls.ended"); case SERVICE_CALL_MISSED: return get("content.service.calls.missed"); + case SERVICE_TIMER_SET: + return getTemplateNamed(senderId, "content.service.encrypted.timer_changed.compact"); + case SERVICE_TIMER_CLEAR: + return getTemplateNamed(senderId, "content.service.encrypted.timer_disabled"); case NONE: return ""; default: @@ -411,6 +439,14 @@ public String formatFullServiceMessage(int senderId, ServiceContent content, boo return get("content.service.calls.ended"); } else if (content instanceof ServiceCallMissed) { return get("content.service.calls.missed"); + } else if (content instanceof ServiceTimerChanged) { + int timer = ((ServiceTimerChanged) content).getTimer(); + if (timer > 0) { + return getTemplateNamed(senderId, "content.service.encrypted.timer_changed.full") + .replace("{timer}", formatDuration(timer)); + } else { + return getTemplateNamed(senderId, "content.service.encrypted.timer_disabled"); + } } return content.getCompatText(); diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/AbsModule.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/AbsModule.java index fbbaeb858c..44aa2a869f 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/AbsModule.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/AbsModule.java @@ -63,6 +63,7 @@ public abstract class AbsModule { public static final String STORAGE_BLOB = "blob"; public static final long BLOB_DIALOGS_ACTIVE = 0; + public static final long BLOB_ENCTYPTED_DIALOGS_ACTIVE_GROUP = 1; private ModuleContext context; diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/ModuleActor.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/ModuleActor.java index 154b79bcf1..d04d09c428 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/ModuleActor.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/ModuleActor.java @@ -72,6 +72,12 @@ public ApiOutPeer buidOutPeer(Peer peer) { return null; } return new ApiOutPeer(ApiPeerType.GROUP, group.getGroupId(), group.getAccessHash()); + } else if (peer.getPeerType() == PeerType.PRIVATE_ENCRYPTED) { + User user = getUser(peer.getPeerId()); + if (user == null) { + return null; + } + return new ApiOutPeer(ApiPeerType.ENCRYPTEDPRIVATE, user.getUid(), user.getAccessHash()); } else { throw new RuntimeException("Unknown peer: " + peer); } @@ -82,6 +88,8 @@ public ApiPeer buildApiPeer(Peer peer) { return new ApiPeer(ApiPeerType.PRIVATE, peer.getPeerId()); } else if (peer.getPeerType() == PeerType.GROUP) { return new ApiPeer(ApiPeerType.GROUP, peer.getPeerId()); + } else if (peer.getPeerType() == PeerType.PRIVATE_ENCRYPTED) { + return new ApiPeer(ApiPeerType.ENCRYPTEDPRIVATE, peer.getPeerId()); } else { return null; } diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/Modules.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/Modules.java index cca0a8b41a..e9e18f104f 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/Modules.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/Modules.java @@ -162,8 +162,8 @@ public void onLoggedIn(boolean first) { profile = new ProfileModule(this); timing.section("Mentions"); mentions = new MentionsModule(this); -// timing.section("Encryption"); -// encryptionModule = new EncryptionModule(this); + timing.section("Encryption"); + encryptionModule = new EncryptionModule(this); timing.section("DisplayLists"); displayLists = new DisplayLists(this); timing.section("EventBus"); @@ -183,8 +183,8 @@ public void onLoggedIn(boolean first) { search.run(); timing.section("Notifications"); notifications.run(); -// timing.section("Encryption"); -// encryptionModule.run(); + timing.section("Encryption"); + encryptionModule.run(); timing.section("Contacts"); contacts.run(); timing.section("Messages"); diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/EncryptedMsgActor.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/EncryptedMsgActor.java deleted file mode 100644 index 9247c9a773..0000000000 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/EncryptedMsgActor.java +++ /dev/null @@ -1,168 +0,0 @@ -package im.actor.core.modules.encryption; - -import java.io.IOException; -import java.util.ArrayList; - -import im.actor.core.api.ApiEncryptedBox; -import im.actor.core.api.ApiEncryptedBoxSignature; -import im.actor.core.api.ApiEncryptedMessage; -import im.actor.core.api.ApiEncyptedBoxKey; -import im.actor.core.api.ApiMessage; -import im.actor.core.entity.Peer; -import im.actor.core.modules.ModuleContext; -import im.actor.core.modules.encryption.entity.EncryptedBox; -import im.actor.core.modules.encryption.entity.EncryptedBoxKey; -import im.actor.core.modules.ModuleActor; -import im.actor.runtime.*; -import im.actor.runtime.Runtime; -import im.actor.runtime.actors.ask.AskCallback; -import im.actor.runtime.function.Function; -import im.actor.runtime.promise.Promise; - -public class EncryptedMsgActor extends ModuleActor { - - private static final String TAG = "MessageEncryptionActor"; - - public EncryptedMsgActor(ModuleContext context) { - super(context); - } - - private Promise doEncrypt(int uid, ApiMessage message) throws IOException { - Log.d(TAG, "doEncrypt"); - -// return ask(context().getEncryption().getEncryptedChatManager(uid), new EncryptedPeerActor.EncryptBox(message.buildContainer())) -// .map(new Function() { -// @Override -// public EncryptedMessage apply(EncryptedPeerActor.EncryptBoxResponse encryptBoxResponse) { -// Log.d(TAG, "doEncrypt:onResult"); -// ArrayList boxKeys = new ArrayList(); -// for (EncryptedBoxKey b : encryptBoxResponse.getBox().getKeys()) { -// boxKeys.add(new ApiEncyptedBoxKey(b.getUid(), -// b.getKeyGroupId(), "curve25519", b.getEncryptedKey())); -// } -// ApiEncryptedBox apiEncryptedBox = new ApiEncryptedBox(0, boxKeys, "aes-kuznechik", encryptBoxResponse.getBox().getEncryptedPackage(), -// new ArrayList()); -// ApiEncryptedMessage apiEncryptedMessage = new ApiEncryptedMessage(apiEncryptedBox); -// return new EncryptedMessage(apiEncryptedMessage); -// } -// }); - - // TODO: Implement - return null; - } - - public void onDecrypt(int uid, ApiEncryptedMessage message) { - Log.d(TAG, "onDecrypt:" + uid); - final long start = im.actor.runtime.Runtime.getActorTime(); - ArrayList encryptedBoxKeys = new ArrayList(); - for (ApiEncyptedBoxKey key : message.getBox().getKeys()) { - if (key.getUsersId() == myUid()) { - encryptedBoxKeys.add(new EncryptedBoxKey(key.getUsersId(), key.getKeyGroupId(), - key.getAlgType(), key.getEncryptedKey())); - } - } - final EncryptedBox encryptedBox = new EncryptedBox(encryptedBoxKeys.toArray(new EncryptedBoxKey[0]), message.getBox().getEncPackage()); - - // TODO: Implement -// ask(context().getEncryption().getEncryptedChatManager(uid), new EncryptedPeerActor.DecryptBox(encryptedBox), new AskCallback() { -// @Override -// public void onResult(Object obj) { -// Log.d(TAG, "onDecrypt:onResult in " + (Runtime.getActorTime() - start) + " ms"); -// EncryptedPeerActor.DecryptBoxResponse re = (EncryptedPeerActor.DecryptBoxResponse) obj; -// try { -// ApiMessage message = ApiMessage.fromBytes(re.getData()); -// Log.d(TAG, "onDecrypt:onResult " + message); -// } catch (IOException e) { -// e.printStackTrace(); -// } -// } -// -// @Override -// public void onError(Exception e) { -// Log.d(TAG, "onDecrypt:onError"); -// e.printStackTrace(); -// } -// }); - } - - @Override - public Promise onAsk(Object message) throws Exception { - if (message instanceof EncryptMessage) { - return doEncrypt(((EncryptMessage) message).getUid(), ((EncryptMessage) message).getMessage()); - } else { - return super.onAsk(message); - } - } - - @Override - public void onReceive(Object message) { - Log.d(TAG, "msg: " + message); - if (message instanceof InMessage) { - InMessage inMessage = (InMessage) message; - onDecrypt(inMessage.senderUid, inMessage.encryptedMessage); - } else { - super.onReceive(message); - } - } - - public static class InMessage { - - private Peer peer; - private long date; - private int senderUid; - private long rid; - private ApiEncryptedMessage encryptedMessage; - - public InMessage(Peer peer, long date, int senderUid, long rid, ApiEncryptedMessage encryptedMessage) { - this.peer = peer; - this.date = date; - this.senderUid = senderUid; - this.rid = rid; - this.encryptedMessage = encryptedMessage; - } - } - - public static class EncryptMessage { - - private int uid; - private ApiMessage message; - - public EncryptMessage(int uid, ApiMessage message) { - this.uid = uid; - this.message = message; - } - - public int getUid() { - return uid; - } - - public ApiMessage getMessage() { - return message; - } - } - - public static class EncryptedMessage { - private ApiEncryptedMessage encryptedMessage; - - public EncryptedMessage(ApiEncryptedMessage encryptedMessage) { - this.encryptedMessage = encryptedMessage; - } - - public ApiEncryptedMessage getEncryptedMessage() { - return encryptedMessage; - } - } - - public static class DecryptMessage { - - private ApiEncryptedMessage encryptedMessage; - - public DecryptMessage(ApiEncryptedMessage encryptedMessage) { - this.encryptedMessage = encryptedMessage; - } - - public ApiEncryptedMessage getEncryptedMessage() { - return encryptedMessage; - } - } -} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/EncryptedPeerActor.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/EncryptedPeerActor.java deleted file mode 100644 index 25d395421b..0000000000 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/EncryptedPeerActor.java +++ /dev/null @@ -1,419 +0,0 @@ -package im.actor.core.modules.encryption; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; - -import im.actor.core.entity.encryption.PeerSession; -import im.actor.core.modules.ModuleContext; -import im.actor.core.modules.encryption.entity.EncryptedBox; -import im.actor.core.modules.encryption.entity.EncryptedBoxKey; -import im.actor.core.modules.encryption.entity.UserKeys; -import im.actor.core.modules.encryption.entity.UserKeysGroup; -import im.actor.core.modules.ModuleActor; -import im.actor.core.util.RandomUtils; -import im.actor.runtime.*; -import im.actor.runtime.Runtime; -import im.actor.runtime.actors.ActorCreator; -import im.actor.runtime.actors.ActorRef; -import im.actor.runtime.actors.Props; -import im.actor.runtime.actors.ask.AskMessage; -import im.actor.runtime.actors.ask.AskResult; -import im.actor.runtime.crypto.Cryptos; -import im.actor.runtime.crypto.IntegrityException; -import im.actor.runtime.crypto.primitives.prf.PRF; -import im.actor.runtime.function.Consumer; -import im.actor.runtime.function.Function; -import im.actor.runtime.function.Predicate; -import im.actor.runtime.promise.Promise; -import im.actor.runtime.crypto.box.ActorBox; -import im.actor.runtime.crypto.box.ActorBoxKey; -import im.actor.runtime.crypto.primitives.util.ByteStrings; -import im.actor.runtime.promise.Promises; -import im.actor.runtime.promise.PromisesArray; -import im.actor.runtime.function.Tuple2; - -import static im.actor.runtime.promise.Promise.success; - -public class EncryptedPeerActor extends ModuleActor { - - private final String TAG; - - private final int uid; - - private int ownKeyGroupId; - private UserKeys theirKeys; - - private HashMap activeSessions = new HashMap<>(); - private HashSet ignoredKeyGroups = new HashSet<>(); - - private boolean isReady = false; - private KeyManagerInt keyManager; - - private final PRF keyPrf = Cryptos.PRF_SHA_STREEBOG_256(); - - public EncryptedPeerActor(int uid, ModuleContext context) { - super(context); - this.uid = uid; - TAG = "EncryptedPeerActor#" + uid; - } - - @Override - public void preStart() { - super.preStart(); - - keyManager = context().getEncryption().getKeyManagerInt(); - - Promises.tuple( - keyManager.getOwnIdentity(), - keyManager.getUserKeyGroups(uid)) - .then(new Consumer>() { - @Override - public void apply(Tuple2 res) { - Log.d(TAG, "then"); - ownKeyGroupId = res.getT1().getKeyGroup(); - theirKeys = res.getT2(); - isReady = true; - unstashAll(); - } - }) - .failure(new Consumer() { - @Override - public void apply(Exception e) { - Log.w(TAG, "Unable to fetch initial parameters"); - Log.e(TAG, e); - } - }); - } - - private Promise doEncrypt(final byte[] data) { - - if (!isReady) { - stash(); - return null; - } - - // - // Stage 1: Loading User Key Groups - // Stage 2: Pick sessions for encryption - // Stage 3: Encrypt box_key int session - // Stage 4: Encrypt box - // - final byte[] encKey = Crypto.randomBytes(32); - final byte[] encKeyExtended = keyPrf.calculate(encKey, "ActorPackage", 128); - Log.d(TAG, "doEncrypt"); - final long start = Runtime.getActorTime(); - return PromisesArray.of(theirKeys.getUserKeysGroups()) - .filter(new Predicate() { - @Override - public boolean apply(UserKeysGroup keysGroup) { - return !ignoredKeyGroups.contains(keysGroup.getKeyGroupId()); - } - }) - .mapOptional(new Function>() { - @Override - public Promise apply(final UserKeysGroup keysGroup) { - if (activeSessions.containsKey(keysGroup.getKeyGroupId())) { - return success(activeSessions.get(keysGroup.getKeyGroupId()).getSessions().get(0)); - } - return context().getEncryption().getSessionManagerInt() - .pickSession(uid, keysGroup.getKeyGroupId()) - .failure(new Consumer() { - @Override - public void apply(Exception e) { - ignoredKeyGroups.add(keysGroup.getKeyGroupId()); - } - }) - .map(new Function() { - @Override - public SessionActor apply(PeerSession src) { - return spawnSession(src); - } - }); - } - }) - .mapOptional(encrypt(encKeyExtended)) - .zip() - .map(new Function, EncryptBoxResponse>() { - @Override - public EncryptBoxResponse apply(List src) { - - if (src.size() == 0) { - throw new RuntimeException("No sessions available"); - } - - Log.d(TAG, "Keys Encrypted in " + (Runtime.getActorTime() - start) + " ms"); - - ArrayList encryptedKeys = new ArrayList<>(); - for (EncryptedSessionActor.EncryptedPackageRes r : src) { - Log.d(TAG, "Keys: " + r.getKeyGroupId()); - encryptedKeys.add(new EncryptedBoxKey(uid, r.getKeyGroupId(), "curve25519", r.getData())); - } - - byte[] encData; - try { - encData = ActorBox.closeBox(ByteStrings.intToBytes(ownKeyGroupId), data, Crypto.randomBytes(32), new ActorBoxKey(encKeyExtended)); - } catch (IntegrityException e) { - e.printStackTrace(); - throw new RuntimeException(e); - } - - Log.d(TAG, "All Encrypted in " + (Runtime.getActorTime() - start) + " ms"); - - return new EncryptBoxResponse(new EncryptedBox( - encryptedKeys.toArray(new EncryptedBoxKey[encryptedKeys.size()]), - ByteStrings.merge(ByteStrings.intToBytes(ownKeyGroupId), encData))); - } - }); - } - - private Promise doDecrypt(final EncryptedBox data) { - - if (!isReady) { - stash(); - return null; - } - - final int senderKeyGroup = ByteStrings.bytesToInt(ByteStrings.substring(data.getEncryptedPackage(), 0, 4)); - final byte[] encPackage = ByteStrings.substring(data.getEncryptedPackage(), 4, data.getEncryptedPackage().length - 4); - - // - // Picking session - // - - if (ignoredKeyGroups.contains(senderKeyGroup)) { - throw new RuntimeException("This key group is ignored"); - } - - return PromisesArray.of(data.getKeys()) - .filter(EncryptedBoxKey.FILTER(myUid(), ownKeyGroupId)) - .first() - .flatMap(new Function>>() { - @Override - public Promise> apply(final EncryptedBoxKey boxKey) { - final long senderPreKeyId = ByteStrings.bytesToLong(boxKey.getEncryptedKey(), 4); - final long receiverPreKeyId = ByteStrings.bytesToLong(boxKey.getEncryptedKey(), 12); - - if (activeSessions.containsKey(boxKey.getKeyGroupId())) { - for (SessionActor s : activeSessions.get(senderKeyGroup).getSessions()) { - if (s.getSession().getOwnPreKeyId() == receiverPreKeyId && - s.getSession().getTheirPreKeyId() == senderPreKeyId) { - return success(new Tuple2<>(s, boxKey)); - } - } - } - return context().getEncryption().getSessionManagerInt() - .pickSession(uid, senderKeyGroup, receiverPreKeyId, senderPreKeyId) - .map(new Function>() { - @Override - public Tuple2 apply(PeerSession src) { - return new Tuple2<>(spawnSession(src), boxKey); - } - }); - } - }) - .flatMap(new Function, Promise>() { - @Override - public Promise apply(Tuple2 src) { - Log.d(TAG, "Key size:" + src.getT2().getEncryptedKey().length); - // return ask(src.getT1().getActorRef(), new EncryptedSessionActor.DecryptPackage(src.getT2().getEncryptedKey())); - // TODO: Implement - return null; - } - }) - .map(new Function() { - @Override - public DecryptBoxResponse apply(EncryptedSessionActor.DecryptedPackage decryptedPackage) { - byte[] encData; - try { - byte[] encKeyExtended = decryptedPackage.getData().length >= 128 - ? decryptedPackage.getData() - : keyPrf.calculate(decryptedPackage.getData(), "ActorPackage", 128); - encData = ActorBox.openBox(ByteStrings.intToBytes(senderKeyGroup), encPackage, new ActorBoxKey(encKeyExtended)); - Log.d(TAG, "Box size: " + encData.length); - } catch (IOException e) { - e.printStackTrace(); - throw new RuntimeException(e); - } - return new DecryptBoxResponse(encData); - } - }); - } - - private void onKeysUpdated(UserKeys userKeys) { - if (!isReady) { - stash(); - return; - } - - this.theirKeys = userKeys; - } - - private SessionActor spawnSession(final PeerSession session) { - - ActorRef res = system().actorOf(Props.create(new ActorCreator() { - @Override - public EncryptedSessionActor create() { - return new EncryptedSessionActor(context(), session); - } - }), getPath() + "/k_" + RandomUtils.nextRid()); - - SessionActor cont = new SessionActor(res, session); - - if (activeSessions.containsKey(session.getTheirKeyGroupId())) { - activeSessions.get(session.getTheirKeyGroupId()).getSessions().add(cont); - } else { - ArrayList l = new ArrayList<>(); - l.add(cont); - activeSessions.put(session.getTheirKeyGroupId(), new SessionHolder(session.getTheirKeyGroupId(), l)); - } - return cont; - } - - private Function> encrypt(final byte[] encKey) { - return new Function>() { - @Override - public Promise apply(SessionActor sessionActor) { - // return ask(sessionActor.getActorRef(), new EncryptedSessionActor.EncryptPackage(encKey)); - // TODO: Implement - return null; - } - }; - } - - // - // Messages - // - - @Override - public Promise onAsk(Object message) throws Exception { - if (message instanceof EncryptBox) { - if (!isReady) { - stash(); - return null; - } - return doEncrypt(((EncryptBox) message).getData()); - } else if (message instanceof DecryptBox) { - if (!isReady) { - stash(); - return null; - } - return doDecrypt(((DecryptBox) message).getEncryptedBox()); - } else { - return super.onAsk(message); - } - } - - @Override - public void onReceive(Object message) { - if (message instanceof KeyGroupUpdated) { - onKeysUpdated(((KeyGroupUpdated) message).getUserKeys()); - } else { - super.onReceive(message); - } - } - - public static class EncryptBox implements AskMessage { - private byte[] data; - - public EncryptBox(byte[] data) { - this.data = data; - } - - public byte[] getData() { - return data; - } - } - - public static class EncryptBoxResponse extends AskResult { - - private EncryptedBox box; - - public EncryptBoxResponse(EncryptedBox box) { - this.box = box; - } - - public EncryptedBox getBox() { - return box; - } - } - - public static class DecryptBox implements AskMessage { - - private EncryptedBox encryptedBox; - - public DecryptBox(EncryptedBox encryptedBox) { - this.encryptedBox = encryptedBox; - } - - public EncryptedBox getEncryptedBox() { - return encryptedBox; - } - } - - public static class DecryptBoxResponse extends AskResult { - - private byte[] data; - - public DecryptBoxResponse(byte[] data) { - this.data = data; - } - - public byte[] getData() { - return data; - } - } - - private class SessionHolder { - - private int keyGroupId; - private ArrayList sessions; - - public SessionHolder(int keyGroupId, ArrayList sessions) { - this.keyGroupId = keyGroupId; - this.sessions = sessions; - } - - public int getKeyGroupId() { - return keyGroupId; - } - - public ArrayList getSessions() { - return sessions; - } - } - - private class SessionActor { - - private ActorRef actorRef; - private PeerSession session; - - public SessionActor(ActorRef actorRef, PeerSession session) { - this.actorRef = actorRef; - this.session = session; - } - - public ActorRef getActorRef() { - return actorRef; - } - - public PeerSession getSession() { - return session; - } - } - - public static class KeyGroupUpdated { - - private UserKeys userKeys; - - public KeyGroupUpdated(UserKeys userKeys) { - this.userKeys = userKeys; - } - - public UserKeys getUserKeys() { - return userKeys; - } - } -} \ No newline at end of file diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/EncryptedProcessor.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/EncryptedProcessor.java deleted file mode 100644 index b416b7ace4..0000000000 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/EncryptedProcessor.java +++ /dev/null @@ -1,35 +0,0 @@ -package im.actor.core.modules.encryption; - -import im.actor.core.modules.AbsModule; -import im.actor.core.modules.ModuleContext; -import im.actor.core.modules.sequence.processor.SequenceProcessor; -import im.actor.core.network.parser.Update; -import im.actor.runtime.actors.messages.Void; -import im.actor.runtime.promise.Promise; - -public class EncryptedProcessor extends AbsModule implements SequenceProcessor { - - public EncryptedProcessor(ModuleContext context) { - super(context); - } - - @Override - public Promise process(Update update) { -// if (update instanceof UpdatePublicKeyGroupAdded) { -// context().getEncryption().getKeyManager().send(new KeyManagerActor.PublicKeysGroupAdded( -// ((UpdatePublicKeyGroupAdded) update).getUid(), -// ((UpdatePublicKeyGroupAdded) update).getKeyGroup() -// )); -// return true; -// } else if (update instanceof UpdatePublicKeyGroupRemoved) { -// context().getEncryption().getKeyManager().send(new KeyManagerActor.PublicKeysGroupRemoved( -// ((UpdatePublicKeyGroupRemoved) update).getUid(), -// ((UpdatePublicKeyGroupRemoved) update).getKeyGroupId() -// )); -// return true; -// } else if (update instanceof UpdateEncryptedPackage) { -// -// } - return null; - } -} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/EncryptedRouter.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/EncryptedRouter.java new file mode 100644 index 0000000000..b93a78ea90 --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/EncryptedRouter.java @@ -0,0 +1,36 @@ +package im.actor.core.modules.encryption; + +import org.jetbrains.annotations.NotNull; + +import im.actor.core.api.ApiEncryptedBox; +import im.actor.core.api.ApiEncryptedContent; +import im.actor.core.api.ApiEncryptionKeyGroup; +import im.actor.core.modules.ModuleContext; +import im.actor.runtime.actors.ActorInterface; +import im.actor.runtime.actors.messages.Void; +import im.actor.runtime.promise.Promise; + +import static im.actor.runtime.actors.ActorSystem.system; + +public class EncryptedRouter extends ActorInterface { + + public EncryptedRouter(ModuleContext context) { + super(system().actorOf("encryption/router", () -> new EncryptedRouterActor(context))); + } + + public Promise onKeyGroupAdded(int uid, ApiEncryptionKeyGroup group) { + return ask(new EncryptedRouterActor.KeyGroupAdded(uid, group)); + } + + public Promise onKeyGroupRemoved(int uid, int keyGroupId) { + return ask(new EncryptedRouterActor.KeyGroupRemoved(uid, keyGroupId)); + } + + public Promise onEncryptedUpdate(int uid, long date, ApiEncryptedContent update) { + return ask(new EncryptedRouterActor.EncryptedUpdate(uid, date, update)); + } + + public Promise onEncryptedBox(long date, int senderId, @NotNull ApiEncryptedBox encryptedBox) { + return ask(new EncryptedRouterActor.EncryptedPackageUpdate(date, senderId, encryptedBox)); + } +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/EncryptedRouterActor.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/EncryptedRouterActor.java new file mode 100644 index 0000000000..cd9425d223 --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/EncryptedRouterActor.java @@ -0,0 +1,200 @@ +package im.actor.core.modules.encryption; + +import org.jetbrains.annotations.NotNull; + +import im.actor.core.api.ApiEncryptedBox; +import im.actor.core.api.ApiEncryptedChatTimerSet; +import im.actor.core.api.ApiEncryptedContent; +import im.actor.core.api.ApiEncryptionKeyGroup; +import im.actor.core.api.ApiServiceMessage; +import im.actor.core.api.ApiServiceTimerChanged; +import im.actor.core.entity.EncryptedConversationState; +import im.actor.core.entity.Message; +import im.actor.core.entity.MessageState; +import im.actor.core.entity.Peer; +import im.actor.core.entity.content.AbsContent; +import im.actor.core.modules.ModuleActor; +import im.actor.core.modules.ModuleContext; +import im.actor.core.modules.encryption.ratchet.EncryptedMsg; +import im.actor.core.modules.encryption.ratchet.KeyManager; +import im.actor.core.modules.encryption.updates.EncryptedUpdates; +import im.actor.runtime.actors.ask.AskMessage; +import im.actor.runtime.actors.messages.Void; +import im.actor.runtime.promise.Promise; +import im.actor.runtime.storage.KeyValueEngine; + +public class EncryptedRouterActor extends ModuleActor { + + // j2objc workaround + private static final Void DUMB = null; + + private KeyManager keyManager; + private EncryptedMsg encryptedMsg; + private EncryptedUpdates updates; + private KeyValueEngine stateKeyValue; + + public EncryptedRouterActor(ModuleContext context) { + super(context); + } + + @Override + public void preStart() { + super.preStart(); + updates = new EncryptedUpdates(context()); + keyManager = context().getEncryption().getKeyManager(); + encryptedMsg = context().getEncryption().getEncryption(); + stateKeyValue = context().getEncryption().getConversationState().getEngine(); + } + + public Promise onKeyGroupAdded(int uid, ApiEncryptionKeyGroup group) { + return keyManager.onKeyGroupAdded(uid, group); + } + + public Promise onKeyGroupRemoved(int uid, int keyGroupId) { + return keyManager.onKeyGroupRemoved(uid, keyGroupId); + } + + public Promise onTimerSet(long randomId, long date, int uid, int timerInMs) { + EncryptedConversationState state = stateKeyValue.getValue(uid); + if (state.getTimer() != timerInMs && state.getTimerDate() < date) { + stateKeyValue.addOrUpdateItem(state.editTimer(timerInMs, date)); + } + + return context().getMessagesModule().getRouter() + .onNewMessage(Peer.secret(uid), new Message(randomId, date, date, + myUid(), MessageState.SENT, AbsContent.fromMessage(new ApiServiceMessage("Timer set", + new ApiServiceTimerChanged(timerInMs))))); + } + + // Messages + + public Promise onEncryptedUpdate(int uid, long date, ApiEncryptedContent update) { + Promise res = Promise.success(null); + if (update instanceof ApiEncryptedChatTimerSet) { + ApiEncryptedChatTimerSet timerSet = (ApiEncryptedChatTimerSet) update; + int peerId = uid; + if (timerSet.getReceiverId() != myUid()) { + peerId = timerSet.getReceiverId(); + } + int timer = 0; + if (timerSet.getTimerMs() != null) { + timer = timerSet.getTimerMs(); + } + res = onTimerSet(timerSet.getRid(), date, peerId, timer); + } + return res.chain(r -> updates.onUpdate(uid, date, update)); + } + + public Promise onEncryptedBox(long date, int senderId, @NotNull ApiEncryptedBox encryptedBox) { + return encryptedMsg.decrypt(senderId, encryptedBox) + .flatMap(message -> onEncryptedUpdate(senderId, date, message)) + .fallback(e -> Promise.success(null)); + } + + @Override + public Promise onAsk(Object message) throws Exception { + if (message instanceof KeyGroupAdded) { + KeyGroupAdded groupAdded = (KeyGroupAdded) message; + return onKeyGroupAdded(groupAdded.getUid(), groupAdded.getGroup()); + } else if (message instanceof KeyGroupRemoved) { + KeyGroupRemoved removed = (KeyGroupRemoved) message; + return onKeyGroupRemoved(removed.getUid(), removed.getKeyGroupId()); + } else if (message instanceof EncryptedUpdate) { + EncryptedUpdate update = (EncryptedUpdate) message; + return onEncryptedUpdate(update.getUid(), update.getDate(), update.getUpdate()); + } else if (message instanceof EncryptedPackageUpdate) { + EncryptedPackageUpdate update = (EncryptedPackageUpdate) message; + return onEncryptedBox(update.getDate(), update.getSenderId(), update.getEncryptedBox()); + } else { + return super.onAsk(message); + } + } + + public static class KeyGroupAdded implements AskMessage { + + private int uid; + private ApiEncryptionKeyGroup group; + + public KeyGroupAdded(int uid, ApiEncryptionKeyGroup group) { + this.uid = uid; + this.group = group; + } + + public int getUid() { + return uid; + } + + public ApiEncryptionKeyGroup getGroup() { + return group; + } + } + + public static class KeyGroupRemoved implements AskMessage { + + private int uid; + private int keyGroupId; + + public KeyGroupRemoved(int uid, int keyGroupId) { + this.uid = uid; + this.keyGroupId = keyGroupId; + } + + public int getUid() { + return uid; + } + + public int getKeyGroupId() { + return keyGroupId; + } + } + + public static class EncryptedUpdate implements AskMessage { + + private int uid; + private long date; + private ApiEncryptedContent update; + + public EncryptedUpdate(int uid, long date, ApiEncryptedContent update) { + this.uid = uid; + this.date = date; + this.update = update; + } + + public int getUid() { + return uid; + } + + public long getDate() { + return date; + } + + public ApiEncryptedContent getUpdate() { + return update; + } + } + + public static class EncryptedPackageUpdate implements AskMessage { + + private long date; + private int senderId; + private ApiEncryptedBox encryptedBox; + + public EncryptedPackageUpdate(long date, int senderId, @NotNull ApiEncryptedBox encryptedBox) { + this.date = date; + this.senderId = senderId; + this.encryptedBox = encryptedBox; + } + + public long getDate() { + return date; + } + + public int getSenderId() { + return senderId; + } + + public ApiEncryptedBox getEncryptedBox() { + return encryptedBox; + } + } +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/EncryptedSessionActor.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/EncryptedSessionActor.java deleted file mode 100644 index f7fbd5094c..0000000000 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/EncryptedSessionActor.java +++ /dev/null @@ -1,296 +0,0 @@ -package im.actor.core.modules.encryption; - -import java.util.ArrayList; - -import im.actor.core.entity.encryption.PeerSession; -import im.actor.core.modules.ModuleContext; -import im.actor.core.modules.encryption.session.EncryptedSessionChain; -import im.actor.core.modules.ModuleActor; -import im.actor.runtime.*; -import im.actor.runtime.actors.ask.AskMessage; -import im.actor.runtime.actors.ask.AskResult; -import im.actor.runtime.function.Consumer; -import im.actor.runtime.function.Function; -import im.actor.runtime.promise.Promise; -import im.actor.runtime.crypto.Curve25519; -import im.actor.runtime.crypto.IntegrityException; -import im.actor.runtime.crypto.primitives.util.ByteStrings; - -import static im.actor.runtime.promise.Promise.success; - -/** - * Axolotl Ratchet encryption session - * Session is identified by: - * 1) Destination User's Id - * 2) Own Key Group Id - * 3) Own Pre Key Id - * 4) Their Key Group Id - * 5) Their Pre Key Id - *

- * During actor starting it downloads all required key from Key Manager. - * To encrypt/decrypt messages this actor spawns encryption chains. - */ -public class EncryptedSessionActor extends ModuleActor { - - private final String TAG; - - // No need to keep too much decryption chains as all messages are sequenced. Newer messages - // never intentionally use old keys, but there are cases when some messages can be sent with - // old encryption keys right after messages with new one. Even when we will kill sequence - // new actors can be easily started again with same keys. - // TODO: Check if this can cause race condition - private final int MAX_DECRYPT_CHAINS = 2; - - // - // Key References - // - - private final int uid; - private final PeerSession session; - - // - // Key Manager reference - // - - private KeyManagerInt keyManager; - - // - // Temp encryption chains - // - - private byte[] latestTheirEphemeralKey; - private ArrayList encryptionChains = new ArrayList<>(); - private ArrayList decryptionChains = new ArrayList<>(); - - // - // Constructors and Methods - // - - public EncryptedSessionActor(ModuleContext context, PeerSession session) { - super(context); - this.TAG = "EncryptionSessionActor#" + session.getUid() + "_" + session.getTheirKeyGroupId(); - this.uid = session.getUid(); - this.session = session; - } - - @Override - public void preStart() { - super.preStart(); - keyManager = context().getEncryption().getKeyManagerInt(); - } - - private Promise onEncrypt(final byte[] data) { - - // - // Stage 1: Pick Their Ephemeral key. Use already received or pick random pre key. - // Stage 2: Pick Encryption Chain - // Stage 3: Decrypt - // - - return success(latestTheirEphemeralKey) - .mapIfNullPromise(keyManager.supplyUserPreKey(uid, session.getTheirKeyGroupId())) - .map(new Function() { - @Override - public EncryptedSessionChain apply(byte[] publicKey) { - return pickEncryptChain(publicKey); - } - }) - .map(new Function() { - @Override - public EncryptedPackageRes apply(EncryptedSessionChain encryptedSessionChain) { - return encrypt(encryptedSessionChain, data); - } - }); - } - - private Promise onDecrypt(final byte[] data) { - - // - // Stage 1: Parsing message header - // Stage 2: Picking decryption chain - // Stage 3: Decryption of message - // Stage 4: Saving their ephemeral key - // - - // final int ownKeyGroupId = ByteStrings.bytesToInt(data, 0); - // final long ownEphemeralKey0Id = ByteStrings.bytesToLong(data, 4); - // final long theirEphemeralKey0Id = ByteStrings.bytesToLong(data, 12); - final byte[] senderEphemeralKey = ByteStrings.substring(data, 20, 32); - final byte[] receiverEphemeralKey = ByteStrings.substring(data, 52, 32); - Log.d(TAG, "Sender Ephemeral " + Crypto.keyHash(senderEphemeralKey)); - Log.d(TAG, "Receiver Ephemeral " + Crypto.keyHash(receiverEphemeralKey)); - - return pickDecryptChain(senderEphemeralKey, receiverEphemeralKey) - .map(new Function() { - @Override - public DecryptedPackage apply(EncryptedSessionChain encryptedSessionChain) { - return decrypt(encryptedSessionChain, data); - } - }) - .then(new Consumer() { - @Override - public void apply(DecryptedPackage decryptedPackage) { - Log.d(TAG, "onDecrypted"); - latestTheirEphemeralKey = senderEphemeralKey; - } - }) - .failure(new Consumer() { - @Override - public void apply(Exception e) { - Log.d(TAG, "onError"); - } - }); - } - - private EncryptedSessionChain pickEncryptChain(byte[] ephemeralKey) { - - if (latestTheirEphemeralKey == null) { - latestTheirEphemeralKey = ephemeralKey; - } - - if (encryptionChains.size() > 0) { - return encryptionChains.get(0); - } - - - EncryptedSessionChain chain = new EncryptedSessionChain(session, Curve25519.keyGenPrivate(Crypto.randomBytes(32)), ephemeralKey); - encryptionChains.add(0, chain); - - return chain; - } - - private EncryptedPackageRes encrypt(EncryptedSessionChain chain, byte[] data) { - - byte[] encrypted; - try { - encrypted = chain.encrypt(data); - } catch (IntegrityException e) { - e.printStackTrace(); - throw new RuntimeException(e); - } - - Log.d(TAG, "!Sender Ephemeral " + Crypto.keyHash(Curve25519.keyGenPublic(chain.getOwnPrivateKey()))); - Log.d(TAG, "!Receiver Ephemeral " + Crypto.keyHash(chain.getTheirPublicKey())); - - return new EncryptedPackageRes(encrypted, session.getTheirKeyGroupId()); - } - - private Promise pickDecryptChain(final byte[] theirEphemeralKey, final byte[] ephemeralKey) { - EncryptedSessionChain pickedChain = null; - for (EncryptedSessionChain c : decryptionChains) { - if (ByteStrings.isEquals(Curve25519.keyGenPublic(c.getOwnPrivateKey()), ephemeralKey)) { - pickedChain = c; - break; - } - } - return success(pickedChain) - .flatMap(new Function>() { - @Override - public Promise apply(EncryptedSessionChain src) { - if (src != null) { - return success(src); - } - - // TODO: Implement! - return null; -// return ask(context().getEncryption().getKeyManager(), new FetchOwnPreKeyByPublic(ephemeralKey)) -// .map(new Function() { -// @Override -// public EncryptedSessionChain apply(PrivateKey src) { -// EncryptedSessionChain chain = new EncryptedSessionChain(session, src.getKey(), theirEphemeralKey); -// decryptionChains.add(0, chain); -// if (decryptionChains.size() > MAX_DECRYPT_CHAINS) { -// decryptionChains.remove(MAX_DECRYPT_CHAINS) -// .safeErase(); -// } -// return chain; -// } -// }); - } - }); - } - - private DecryptedPackage decrypt(EncryptedSessionChain chain, byte[] data) { - byte[] decrypted; - try { - decrypted = chain.decrypt(data); - } catch (IntegrityException e) { - e.printStackTrace(); - throw new RuntimeException(e); - } - return new DecryptedPackage(decrypted); - } - - // - // Actor Messages - // - - @Override - public Promise onAsk(Object message) throws Exception { - if (message instanceof EncryptPackage) { - return onEncrypt(((EncryptPackage) message).getData()); - } else if (message instanceof DecryptPackage) { - DecryptPackage decryptPackage = (DecryptPackage) message; - return onDecrypt(decryptPackage.getData()); - } else { - return super.onAsk(message); - } - } - - public static class EncryptPackage implements AskMessage { - private byte[] data; - - public EncryptPackage(byte[] data) { - this.data = data; - } - - public byte[] getData() { - return data; - } - } - - public static class EncryptedPackageRes extends AskResult { - - private byte[] data; - private int keyGroupId; - - public EncryptedPackageRes(byte[] data, int keyGroupId) { - this.data = data; - this.keyGroupId = keyGroupId; - } - - public byte[] getData() { - return data; - } - - public int getKeyGroupId() { - return keyGroupId; - } - } - - public static class DecryptPackage implements AskMessage { - - private byte[] data; - - public DecryptPackage(byte[] data) { - this.data = data; - } - - public byte[] getData() { - return data; - } - } - - public static class DecryptedPackage extends AskResult { - - private byte[] data; - - public DecryptedPackage(byte[] data) { - this.data = data; - } - - public byte[] getData() { - return data; - } - } -} \ No newline at end of file diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/EncryptionModule.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/EncryptionModule.java index 6598e2a20b..b6d2357c23 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/EncryptionModule.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/EncryptionModule.java @@ -1,83 +1,143 @@ package im.actor.core.modules.encryption; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; +import im.actor.core.api.ApiEncryptedBox; +import im.actor.core.api.ApiEncryptedChatTimerSet; +import im.actor.core.api.ApiEncryptedContent; +import im.actor.core.api.ApiUserOutPeer; +import im.actor.core.api.rpc.RequestSendEncryptedPackage; +import im.actor.core.api.rpc.ResponseSendEncryptedPackage; +import im.actor.core.entity.EncryptedConversationState; import im.actor.core.modules.AbsModule; import im.actor.core.modules.ModuleContext; -import im.actor.core.modules.encryption.EncryptedPeerActor; -import im.actor.core.modules.encryption.KeyManagerActor; -import im.actor.core.modules.encryption.EncryptedMsgActor; -import im.actor.core.modules.encryption.KeyManagerInt; -import im.actor.runtime.actors.ActorCreator; -import im.actor.runtime.actors.ActorRef; -import im.actor.runtime.actors.Props; +import im.actor.core.modules.encryption.ratchet.EncryptedMsg; +import im.actor.core.modules.encryption.ratchet.EncryptedUser; +import im.actor.core.modules.encryption.ratchet.EncryptedUserActor; +import im.actor.core.modules.encryption.ratchet.KeyManager; +import im.actor.core.modules.encryption.ratchet.SessionManager; +import im.actor.core.modules.encryption.ratchet.entity.EncryptedMessage; +import im.actor.core.util.RandomUtils; +import im.actor.core.viewmodel.EncryptedConversationVM; +import im.actor.runtime.Storage; +import im.actor.runtime.actors.messages.Void; +import im.actor.runtime.mvvm.MVVMCollection; +import im.actor.runtime.promise.Promise; +import im.actor.runtime.storage.KeyValueEngine; +import im.actor.runtime.storage.KeyValueStorage; import static im.actor.runtime.actors.ActorSystem.system; public class EncryptionModule extends AbsModule { - private ActorRef keyManager; - private KeyManagerInt keyManagerInt; - private ActorRef sessionManager; - private SessionManagerInt sessionManagerInt; + // j2objc workaround + private static final Void DUMB = null; - private ActorRef messageEncryptor; - private HashMap encryptedStates = new HashMap(); + private KeyManager keyManager; + private SessionManager sessionManager; + private EncryptedRouter encryptedRouter; + private EncryptedMsg encryption; + private KeyValueStorage keyValueStorage; + private MVVMCollection conversationState; + private final HashMap users = new HashMap<>(); public EncryptionModule(ModuleContext context) { super(context); } public void run() { - keyManager = system().actorOf(Props.create(new ActorCreator() { - @Override - public KeyManagerActor create() { - return new KeyManagerActor(context()); - } - }), "encryption/keys"); - keyManagerInt = new KeyManagerInt(keyManager); - sessionManager = system().actorOf(Props.create(new ActorCreator() { - @Override - public SessionManagerActor create() { - return new SessionManagerActor(context()); - } - }), "encryption/sessions"); - sessionManagerInt = new SessionManagerInt(sessionManager); - messageEncryptor = system().actorOf(Props.create(new ActorCreator() { - @Override - public EncryptedMsgActor create() { - return new EncryptedMsgActor(context()); - } - }), "encryption/messaging"); + + keyValueStorage = Storage.createKeyValue("session_temp_storage"); + conversationState = Storage.createKeyValue("encrypted_chat_state", EncryptedConversationVM.CREATOR, + EncryptedConversationState.CREATOR, EncryptedConversationState.DEFAULT_CREATOR); + + keyManager = new KeyManager(context()); + sessionManager = new SessionManager(context()); + encryption = new EncryptedMsg(context()); + encryptedRouter = new EncryptedRouter(context()); } - public SessionManagerInt getSessionManagerInt() { - return sessionManagerInt; + public KeyManager getKeyManager() { + return keyManager; } - public ActorRef getMessageEncryptor() { - return messageEncryptor; + public EncryptedRouter getRouter() { + return encryptedRouter; } - public ActorRef getKeyManager() { - return keyManager; + public SessionManager getSessionManager() { + return sessionManager; } - public KeyManagerInt getKeyManagerInt() { - return keyManagerInt; + public EncryptedMsg getEncryption() { + return encryption; } - public ActorRef getEncryptedChatManager(final int uid) { - synchronized (encryptedStates) { - if (!encryptedStates.containsKey(uid)) { - encryptedStates.put(uid, system().actorOf(Props.create(new ActorCreator() { - @Override - public EncryptedPeerActor create() { - return new EncryptedPeerActor(uid, context()); - } - }), "encryption/uid_" + uid)); + public EncryptedUser getEncryptedUser(int uid) { + synchronized (users) { + if (!users.containsKey(uid)) { + users.put(uid, new EncryptedUser(system().actorOf("encryption/uid_" + uid, + () -> new EncryptedUserActor(uid, context())))); } - return encryptedStates.get(uid); + return users.get(uid); + } + } + + public KeyValueStorage getKeyValueStorage() { + return keyValueStorage; + } + + public MVVMCollection getConversationState() { + return conversationState; + } + + public Promise onUpdate(int senderId, long date, ApiEncryptedContent update) { + return encryptedRouter.onEncryptedUpdate(senderId, date, update); + } + + public Promise doSend(ApiEncryptedContent content, int uid) { + return doSend(RandomUtils.nextRid(), content, uid); + } + + public Promise doSend(ApiEncryptedContent content, List uids) { + return doSend(RandomUtils.nextRid(), content, uids); + } + + public Promise doSend(long rid, ApiEncryptedContent content, int uid) { + ArrayList receiver = new ArrayList<>(); + receiver.add(uid); + if (uid != myUid()) { + receiver.add(myUid()); } + return doSend(rid, content, receiver); + } + + public Promise doSend(long rid, ApiEncryptedContent content, List uids) { + + ArrayList outPeers = new ArrayList<>(); + for (int i : uids) { + outPeers.add(new ApiUserOutPeer(i, users().getValue(i).getAccessHash())); + } + + return getEncryption().encrypt(uids, content) + .flatMap(m -> api(new RequestSendEncryptedPackage(rid, outPeers, m.getIgnoredGroups(), m.getEncryptedBox()))) + .flatMap(r -> { + if (r.getDate() != null) { + return Promise.success(r.getDate()); + } else { + return getKeyManager().onKeyGroupDiffReceived(r.getMissedKeyGroups(), r.getObsoleteKeyGroups()) + .flatMap(r2 -> doSend(rid, content, uids)); + } + }); + } + + + public Promise setSecretChatTimer(int uid, Integer timeout) { + ApiEncryptedContent encryptedContent = new ApiEncryptedChatTimerSet(uid, + RandomUtils.nextRid(), timeout); + return doSend(encryptedContent, uid) + .flatMap(r -> getRouter().onEncryptedUpdate(myUid(), r, encryptedContent)); } } diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/EncryptionProcessor.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/EncryptionProcessor.java new file mode 100644 index 0000000000..6b8079e2ed --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/EncryptionProcessor.java @@ -0,0 +1,40 @@ +package im.actor.core.modules.encryption; + +import im.actor.core.api.updates.UpdateEncryptedPackage; +import im.actor.core.api.updates.UpdatePublicKeyGroupAdded; +import im.actor.core.api.updates.UpdatePublicKeyGroupRemoved; +import im.actor.core.modules.AbsModule; +import im.actor.core.modules.ModuleContext; +import im.actor.core.modules.sequence.processor.SequenceProcessor; +import im.actor.core.network.parser.Update; +import im.actor.runtime.actors.messages.Void; +import im.actor.runtime.promise.Promise; + +public class EncryptionProcessor extends AbsModule implements SequenceProcessor { + + public EncryptionProcessor(ModuleContext context) { + super(context); + } + + @Override + public Promise process(Update update) { + if (update instanceof UpdatePublicKeyGroupAdded) { + UpdatePublicKeyGroupAdded groupAdded = (UpdatePublicKeyGroupAdded) update; + return context().getEncryption() + .getRouter() + .onKeyGroupAdded(groupAdded.getUid(), groupAdded.getKeyGroup()); + } else if (update instanceof UpdatePublicKeyGroupRemoved) { + UpdatePublicKeyGroupRemoved groupRemoved = (UpdatePublicKeyGroupRemoved) update; + return context().getEncryption() + .getRouter() + .onKeyGroupRemoved(groupRemoved.getUid(), groupRemoved.getKeyGroupId()); + } else if (update instanceof UpdateEncryptedPackage) { + UpdateEncryptedPackage encryptedPackage = (UpdateEncryptedPackage) update; + return context().getEncryption() + .getRouter() + .onEncryptedBox(encryptedPackage.getDate(), + encryptedPackage.getSenderId(), encryptedPackage.getEncryptedBox()); + } + return null; + } +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/KeyManagerInt.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/KeyManagerInt.java deleted file mode 100644 index 351019a02e..0000000000 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/KeyManagerInt.java +++ /dev/null @@ -1,58 +0,0 @@ -package im.actor.core.modules.encryption; - -import im.actor.core.modules.encryption.entity.PrivateKey; -import im.actor.core.modules.encryption.entity.PublicKey; -import im.actor.core.modules.encryption.entity.UserKeys; -import im.actor.runtime.actors.ActorInterface; -import im.actor.runtime.actors.ActorRef; -import im.actor.runtime.function.Function; -import im.actor.runtime.function.Supplier; -import im.actor.runtime.promise.Promise; -import im.actor.runtime.promise.Promises; - -public class KeyManagerInt extends ActorInterface { - - public KeyManagerInt(ActorRef dest) { - super(dest); - } - - public Promise getOwnIdentity() { - return ask(new KeyManagerActor.FetchOwnKey()); - } - - public Promise getUserKeyGroups(int uid) { - return ask(new KeyManagerActor.FetchUserKeys(uid)); - } - - public Promise getUserRandomPreKey(int uid, int keyGroupId) { - return ask(new KeyManagerActor.FetchUserPreKeyRandom(uid, keyGroupId)); - } - - public Promise getUserPreKey(int uid, int keyGroupId, long preKeyId) { - return ask(new KeyManagerActor.FetchUserPreKey(uid, keyGroupId, preKeyId)); - } - - public Promise getOwnRandomPreKey() { - return ask(new KeyManagerActor.FetchOwnRandomPreKey()); - } - - public Promise getOwnPreKey(long id) { - return ask(new KeyManagerActor.FetchOwnPreKeyById(id)); - } - - - public Supplier> supplyUserPreKey(final int uid, final int keyGroupId) { - return new Supplier>() { - @Override - public Promise get() { - return getUserRandomPreKey(uid, keyGroupId) - .map(new Function() { - @Override - public byte[] apply(PublicKey publicKey) { - return publicKey.getPublicKey(); - } - }); - } - }; - } -} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/SessionManagerInt.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/SessionManagerInt.java deleted file mode 100644 index 4760bc6501..0000000000 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/SessionManagerInt.java +++ /dev/null @@ -1,21 +0,0 @@ -package im.actor.core.modules.encryption; - -import im.actor.core.entity.encryption.PeerSession; -import im.actor.runtime.actors.ActorInterface; -import im.actor.runtime.actors.ActorRef; -import im.actor.runtime.promise.Promise; - -public class SessionManagerInt extends ActorInterface { - - public SessionManagerInt(ActorRef dest) { - super(dest); - } - - public Promise pickSession(int uid, int keyGroup) { - return ask(new SessionManagerActor.PickSessionForEncrypt(uid, keyGroup)); - } - - public Promise pickSession(int uid, int keyGroup, long ownKeyId, long theirKeyId) { - return ask(new SessionManagerActor.PickSessionForDecrypt(uid, keyGroup, theirKeyId, ownKeyId)); - } -} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/entity/EncryptedBox.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/entity/EncryptedBox.java deleted file mode 100644 index 1aa49ce492..0000000000 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/entity/EncryptedBox.java +++ /dev/null @@ -1,20 +0,0 @@ -package im.actor.core.modules.encryption.entity; - -public class EncryptedBox { - - private final EncryptedBoxKey[] keys; - private final byte[] encryptedPackage; - - public EncryptedBox(EncryptedBoxKey[] keys, byte[] encryptedPackage) { - this.keys = keys; - this.encryptedPackage = encryptedPackage; - } - - public EncryptedBoxKey[] getKeys() { - return keys; - } - - public byte[] getEncryptedPackage() { - return encryptedPackage; - } -} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/entity/EncryptedBoxKey.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/entity/EncryptedBoxKey.java deleted file mode 100644 index 6e1fdf1b41..0000000000 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/entity/EncryptedBoxKey.java +++ /dev/null @@ -1,43 +0,0 @@ -package im.actor.core.modules.encryption.entity; - -import im.actor.runtime.function.Predicate; - -public class EncryptedBoxKey { - - public static Predicate FILTER(final int myUid, final int keyGroupId) { - return new Predicate() { - @Override - public boolean apply(EncryptedBoxKey boxKey) { - return boxKey.getUid() == myUid && boxKey.getKeyGroupId() == keyGroupId; - } - }; - } - - private final int uid; - private final int keyGroupId; - private final byte[] encryptedKey; - private final String keyAlg; - - public EncryptedBoxKey(int uid, int keyGroupId, String keyAlg, byte[] encryptedKey) { - this.uid = uid; - this.keyGroupId = keyGroupId; - this.encryptedKey = encryptedKey; - this.keyAlg = keyAlg; - } - - public String getKeyAlg() { - return keyAlg; - } - - public int getUid() { - return uid; - } - - public int getKeyGroupId() { - return keyGroupId; - } - - public byte[] getEncryptedKey() { - return encryptedKey; - } -} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/entity/SessionEphemeralKey.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/entity/SessionEphemeralKey.java deleted file mode 100644 index 52b3885345..0000000000 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/entity/SessionEphemeralKey.java +++ /dev/null @@ -1,50 +0,0 @@ -package im.actor.core.modules.encryption.entity; - -import java.io.IOException; - -import im.actor.runtime.bser.BserObject; -import im.actor.runtime.bser.BserValues; -import im.actor.runtime.bser.BserWriter; - -public class SessionEphemeralKey extends BserObject { - - private byte[] publicKey; - private byte[] privateKey; - private long dateCreated; - - public SessionEphemeralKey(byte[] publicKey, byte[] privateKey, long dateCreated) { - this.publicKey = publicKey; - this.privateKey = privateKey; - this.dateCreated = dateCreated; - } - - public SessionEphemeralKey(byte[] data) throws IOException { - load(data); - } - - public byte[] getPublicKey() { - return publicKey; - } - - public byte[] getPrivateKey() { - return privateKey; - } - - public long getDateCreated() { - return dateCreated; - } - - @Override - public void parse(BserValues values) throws IOException { - dateCreated = values.getLong(1); - publicKey = values.getBytes(2); - privateKey = values.optBytes(3); - } - - @Override - public void serialize(BserWriter writer) throws IOException { - writer.writeLong(1, dateCreated); - writer.writeBytes(2, publicKey); - writer.writeBytes(3, privateKey); - } -} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/entity/SessionId.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/entity/SessionId.java deleted file mode 100644 index 5d87f7b01c..0000000000 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/entity/SessionId.java +++ /dev/null @@ -1,64 +0,0 @@ -package im.actor.core.modules.encryption.entity; - -public final class SessionId { - - private int ownKeyGroupId; - private int theirKeyGroupId; - private long ownKeyId0; - private long theirKeyId0; - - public SessionId(int ownKeyGroupId, long ownKeyId0, int theirKeyGroupId, long theirKeyId0) { - this.theirKeyGroupId = theirKeyGroupId; - this.ownKeyId0 = ownKeyId0; - this.theirKeyId0 = theirKeyId0; - this.ownKeyGroupId = ownKeyGroupId; - } - - public int getOwnKeyGroupId() { - return ownKeyGroupId; - } - - public int getTheirKeyGroupId() { - return theirKeyGroupId; - } - - public long getOwnKeyId0() { - return ownKeyId0; - } - - public long getTheirKeyId0() { - return theirKeyId0; - } - - @Override - public String toString() { - return "SessionId{" + - "ownKeyGroupId=" + ownKeyGroupId + - ", theirKeyGroupId=" + theirKeyGroupId + - ", ownKeyId0=" + ownKeyId0 + - ", theirKeyId0=" + theirKeyId0 + - '}'; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - SessionId sessionId = (SessionId) o; - - if (ownKeyGroupId != sessionId.ownKeyGroupId) return false; - if (theirKeyGroupId != sessionId.theirKeyGroupId) return false; - if (ownKeyId0 != sessionId.ownKeyId0) return false; - return theirKeyId0 == sessionId.theirKeyId0; - } - - @Override - public int hashCode() { - int result = ownKeyGroupId; - result = 31 * result + theirKeyGroupId; - result = 31 * result + (int) (ownKeyId0 ^ (ownKeyId0 >>> 32)); - result = 31 * result + (int) (theirKeyId0 ^ (theirKeyId0 >>> 32)); - return result; - } -} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/entity/SessionStorage.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/entity/SessionStorage.java deleted file mode 100644 index ba5ed286b9..0000000000 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/entity/SessionStorage.java +++ /dev/null @@ -1,109 +0,0 @@ -package im.actor.core.modules.encryption.entity; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -import im.actor.runtime.bser.BserObject; -import im.actor.runtime.bser.BserValues; -import im.actor.runtime.bser.BserWriter; - -public class SessionStorage extends BserObject { - - private long sid; - private int uid; - private int ownKeyGroupId; - private int theirKeyGroupId; - private long ownPreKeyId; - private long theirPreKeyId; - - private ArrayList theirKeys; - private ArrayList ownKeys; - - public SessionStorage(long sid, int uid, - int theirKeyGroupId, - int ownKeyGroupId, - long ownPreKeyId, - long theirPreKeyId, - ArrayList theirKeys, - ArrayList ownKeys) { - this.sid = sid; - this.uid = uid; - this.theirKeyGroupId = theirKeyGroupId; - this.ownKeyGroupId = ownKeyGroupId; - this.ownPreKeyId = ownPreKeyId; - this.theirPreKeyId = theirPreKeyId; - this.theirKeys = new ArrayList<>(theirKeys); - this.ownKeys = new ArrayList<>(ownKeys); - } - - public SessionStorage(byte[] data) throws IOException { - load(data); - } - - public long getSid() { - return sid; - } - - public int getUid() { - return uid; - } - - public int getOwnKeyGroupId() { - return ownKeyGroupId; - } - - public int getTheirKeyGroupId() { - return theirKeyGroupId; - } - - public long getOwnPreKeyId() { - return ownPreKeyId; - } - - public long getTheirPreKeyId() { - return theirPreKeyId; - } - - public ArrayList getTheirKeys() { - return theirKeys; - } - - public ArrayList getOwnKeys() { - return ownKeys; - } - - @Override - public void parse(BserValues values) throws IOException { - sid = values.getLong(1); - uid = values.getInt(2); - ownKeyGroupId = values.getInt(3); - theirKeyGroupId = values.getInt(4); - ownPreKeyId = values.getLong(5); - theirPreKeyId = values.getLong(6); - - theirKeys = new ArrayList<>(); - List theirEphemeral = values.getRepeatedBytes(7); - for (byte[] b : theirEphemeral) { - theirKeys.add(new SessionEphemeralKey(b)); - } - - ownKeys = new ArrayList<>(); - List ownEphemeral = values.getRepeatedBytes(8); - for (byte[] b : ownEphemeral) { - theirKeys.add(new SessionEphemeralKey(b)); - } - } - - @Override - public void serialize(BserWriter writer) throws IOException { - writer.writeLong(1, sid); - writer.writeInt(2, uid); - writer.writeInt(3, ownKeyGroupId); - writer.writeInt(4, theirKeyGroupId); - writer.writeLong(5, ownPreKeyId); - writer.writeLong(6, theirPreKeyId); - writer.writeRepeatedObj(7, theirKeys); - writer.writeRepeatedObj(8, ownKeys); - } -} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/entity/UserSessions.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/entity/UserSessions.java deleted file mode 100644 index 73772808db..0000000000 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/entity/UserSessions.java +++ /dev/null @@ -1,46 +0,0 @@ -package im.actor.core.modules.encryption.entity; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -import im.actor.core.entity.encryption.PeerSession; -import im.actor.runtime.bser.BserObject; -import im.actor.runtime.bser.BserValues; -import im.actor.runtime.bser.BserWriter; - -public class UserSessions extends BserObject { - - private int uid; - private ArrayList sessionDescriptors; - - public UserSessions(int uid, ArrayList sessionDescriptors) { - this.uid = uid; - this.sessionDescriptors = sessionDescriptors; - } - - public int getUid() { - return uid; - } - - public ArrayList getSessionDescriptors() { - return sessionDescriptors; - } - - @Override - public void parse(BserValues values) throws IOException { - uid = values.getInt(1); - - sessionDescriptors = new ArrayList<>(); - List desc = values.getRepeatedBytes(2); - for (byte[] b : desc) { - sessionDescriptors.add(new PeerSession(b)); - } - } - - @Override - public void serialize(BserWriter writer) throws IOException { - writer.writeInt(1, uid); - writer.writeRepeatedObj(2, sessionDescriptors); - } -} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/EncryptedMsg.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/EncryptedMsg.java new file mode 100644 index 0000000000..b1ddb821e9 --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/EncryptedMsg.java @@ -0,0 +1,49 @@ +package im.actor.core.modules.encryption.ratchet; + +import java.util.List; + +import im.actor.core.api.ApiEncryptedBox; +import im.actor.core.api.ApiEncryptedContent; +import im.actor.core.modules.ModuleContext; +import im.actor.core.modules.encryption.ratchet.entity.EncryptedMessage; +import im.actor.runtime.actors.ActorInterface; +import im.actor.runtime.promise.Promise; + +import static im.actor.runtime.actors.ActorSystem.system; + +/** + * Entry point for message encryption + */ +public class EncryptedMsg extends ActorInterface { + + /** + * Constructor of encrypted messaging interface + * + * @param context context + */ + public EncryptedMsg(ModuleContext context) { + super(system().actorOf("encryption/messaging", () -> new EncryptedMsgActor(context))); + } + + /** + * Encrypt Message for secret chats + * + * @param uids User's ids. Add own UID for sending to other devices + * @param message message content + * @return promise of encrypted message + */ + public Promise encrypt(List uids, ApiEncryptedContent message) { + return ask(new EncryptedMsgActor.EncryptMessage(message, uids)); + } + + /** + * Decrypt Message from private secret chat + * + * @param uid user's id + * @param encryptedBox encrypted message + * @return promise of decrypted message + */ + public Promise decrypt(int uid, ApiEncryptedBox encryptedBox) { + return ask(new EncryptedMsgActor.DecryptMessage(uid, encryptedBox)); + } +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/EncryptedMsgActor.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/EncryptedMsgActor.java new file mode 100644 index 0000000000..162dbb6155 --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/EncryptedMsgActor.java @@ -0,0 +1,193 @@ +package im.actor.core.modules.encryption.ratchet; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import im.actor.core.api.ApiEncryptedBox; +import im.actor.core.api.ApiEncryptedContent; +import im.actor.core.api.ApiEncryptedData; +import im.actor.core.api.ApiEncyptedBoxKey; +import im.actor.core.api.ApiKeyGroupId; +import im.actor.core.modules.ModuleContext; +import im.actor.core.modules.ModuleActor; +import im.actor.core.modules.encryption.ratchet.entity.EncryptedMessage; +import im.actor.core.modules.encryption.ratchet.entity.EncryptedUserKeys; +import im.actor.core.modules.encryption.ratchet.entity.OwnIdentity; +import im.actor.runtime.Crypto; +import im.actor.runtime.actors.ask.AskMessage; +import im.actor.runtime.bser.Bser; +import im.actor.runtime.crypto.Cryptos; +import im.actor.runtime.crypto.IntegrityException; +import im.actor.runtime.crypto.box.ActorBox; +import im.actor.runtime.crypto.box.ActorBoxKey; +import im.actor.runtime.crypto.primitives.prf.PRF; +import im.actor.runtime.crypto.primitives.util.ByteStrings; +import im.actor.runtime.promise.Promise; +import im.actor.runtime.promise.PromisesArray; + +public class EncryptedMsgActor extends ModuleActor { + + private static final int VERSION = 1; + + private static final String TAG = "MessageEncryptionActor"; + + private final PRF KEY_PRF = Cryptos.PRF_SHA_STREEBOG_256(); + + private boolean isFreezed = false; + private OwnIdentity ownIdentity; + + public EncryptedMsgActor(ModuleContext context) { + super(context); + } + + @Override + public void preStart() { + super.preStart(); + + context().getEncryption().getKeyManager().getOwnIdentity().then(id -> { + ownIdentity = id; + isFreezed = false; + unstashAll(); + }); + } + + private Promise doEncrypt(ApiEncryptedContent message, List uids) throws IOException { + + // Generate Encryption Keys + // byte[] encKey = Crypto.randomBytes(32); + byte[] encKeyExtended = Crypto.randomBytes(128);//KEY_PRF.calculate(encKey, "ActorPackage", 128); + + // Encrypt Data + byte[] encryptedData; + byte[] dataToEncrypt = message.buildContainer(); + byte[] dataHeader = ByteStrings.merge(new byte[]{VERSION}, + ByteStrings.intToBytes(myUid()), + ByteStrings.intToBytes(ownIdentity.getKeyGroup())); + try { + encryptedData = ActorBox.closeBox(dataHeader, dataToEncrypt, Crypto.randomBytes(32), + new ActorBoxKey(encKeyExtended)); + } catch (IntegrityException e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + + // Put To Plain-Text versioned container + byte[] dataContainer = new ApiEncryptedData(VERSION, encryptedData).toByteArray(); + + // Encryption for all required users + return PromisesArray.of(uids) + .map((u) -> Promise.success(context().getEncryption().getEncryptedUser(u))) + .map((u) -> u.encrypt(encKeyExtended)) + .zip((r) -> { + ArrayList boxKeys = new ArrayList<>(); + ArrayList ignored = new ArrayList<>(); + for (EncryptedUserKeys uk : r) { + boxKeys.addAll(uk.getBoxKeys()); + for (Integer i : uk.getIgnoredKeys()) { + ignored.add(new ApiKeyGroupId(uk.getUid(), i)); + } + } + + return new EncryptedMessage(new ApiEncryptedBox(ownIdentity.getKeyGroup(), + boxKeys, "aes-kuznechik", dataContainer, new ArrayList<>()), ignored); + }); + } + + public Promise doDecrypt(int uid, ApiEncryptedBox box) { + + // Loading Package + ApiEncryptedData encData; + try { + encData = Bser.parse(new ApiEncryptedData(), box.getEncPackage()); + } catch (IOException e) { + throw new RuntimeException(e); + } + if (encData.getVersion() != VERSION) { + throw new RuntimeException("Unsupported version " + encData.getVersion()); + } + + return context().getEncryption() + .getEncryptedUser(uid) + .decrypt(box.getSenderKeyGroupId(), box.getKeys()).map(bytes -> { + + // Decryption of package + byte[] dataHeader = ByteStrings.merge( + new byte[]{VERSION}, + ByteStrings.intToBytes(uid), + ByteStrings.intToBytes(box.getSenderKeyGroupId())); + byte[] data; + try { + data = ActorBox.openBox(dataHeader, encData.getData(), new ActorBoxKey(bytes)); + } catch (IntegrityException e) { + throw new RuntimeException(e); + } + + // Parsing content + ApiEncryptedContent content; + try { + content = ApiEncryptedContent.fromBytes(data); + } catch (IOException e) { + throw new RuntimeException(e); + } + return content; + }); + } + + @Override + public Promise onAsk(Object message) throws Exception { + if (message instanceof EncryptMessage) { + if (isFreezed) { + stash(); + return null; + } + return doEncrypt(((EncryptMessage) message).getMessage(), ((EncryptMessage) message).getUids()); + } else if (message instanceof DecryptMessage) { + if (isFreezed) { + stash(); + return null; + } + return doDecrypt(((DecryptMessage) message).getUid(), ((DecryptMessage) message).getEncryptedBox()); + } else { + return super.onAsk(message); + } + } + + public static class EncryptMessage implements AskMessage { + + private ApiEncryptedContent message; + private List uids; + + public EncryptMessage(ApiEncryptedContent message, List uids) { + this.uids = uids; + this.message = message; + } + + public ApiEncryptedContent getMessage() { + return message; + } + + public List getUids() { + return uids; + } + } + + public static class DecryptMessage implements AskMessage { + + private int uid; + private ApiEncryptedBox encryptedBox; + + public DecryptMessage(int uid, ApiEncryptedBox encryptedBox) { + this.uid = uid; + this.encryptedBox = encryptedBox; + } + + public int getUid() { + return uid; + } + + public ApiEncryptedBox getEncryptedBox() { + return encryptedBox; + } + } +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/EncryptedSession.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/EncryptedSession.java new file mode 100644 index 0000000000..7f3fc14696 --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/EncryptedSession.java @@ -0,0 +1,59 @@ +package im.actor.core.modules.encryption.ratchet; + +import im.actor.core.api.ApiEncyptedBoxKey; +import im.actor.core.entity.encryption.PeerSession; +import im.actor.core.modules.ModuleContext; +import im.actor.core.util.RandomUtils; +import im.actor.runtime.actors.ActorInterface; +import im.actor.runtime.promise.Promise; + +import static im.actor.runtime.actors.ActorSystem.system; + +/** + * Double Ratchet encrypted session operations + */ +public class EncryptedSession extends ActorInterface { + + private PeerSession session; + + /** + * Constructor of session encryption + * + * @param session session settings + * @param context context + */ + public EncryptedSession(PeerSession session, ModuleContext context) { + super(system().actorOf("encryption/uid_" + session.getUid() + "/session_" + + RandomUtils.nextRid(), "encrypt", () -> new EncryptedSessionActor(context, session))); + this.session = session; + } + + /** + * Get Peer Session parameters + * + * @return peer session + */ + public PeerSession getSession() { + return session; + } + + /** + * Encrypt data for session + * + * @param data for encryption + * @return promise of encrypted package + */ + public Promise encrypt(byte[] data) { + return ask(new EncryptedSessionActor.EncryptPackage(data)); + } + + /** + * Decrypt data for session + * + * @param key for decryption + * @return promise of decrypted package + */ + public Promise decrypt(ApiEncyptedBoxKey key) { + return ask(new EncryptedSessionActor.DecryptPackage(key)); + } +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/EncryptedSessionActor.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/EncryptedSessionActor.java new file mode 100644 index 0000000000..1bfd863751 --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/EncryptedSessionActor.java @@ -0,0 +1,304 @@ +package im.actor.core.modules.encryption.ratchet; + +import java.io.IOException; +import java.util.ArrayList; + +import im.actor.core.api.ApiEncyptedBoxKey; +import im.actor.core.entity.encryption.PeerSession; +import im.actor.core.modules.ModuleContext; +import im.actor.core.modules.encryption.ratchet.entity.PrivateKey; +import im.actor.core.modules.encryption.ratchet.entity.PublicKey; +import im.actor.core.modules.ModuleActor; +import im.actor.core.modules.encryption.ratchet.entity.SessionState; +import im.actor.runtime.*; +import im.actor.runtime.actors.ask.AskMessage; +import im.actor.runtime.promise.Promise; +import im.actor.runtime.crypto.Curve25519; +import im.actor.runtime.crypto.IntegrityException; +import im.actor.runtime.crypto.primitives.util.ByteStrings; +import im.actor.runtime.storage.KeyValueStorage; + +import static im.actor.runtime.promise.Promise.success; + +/** + * Double Ratchet encryption session + * Session is identified by: + * 1) Destination User's Id + * 2) Own Key Group Id + * 3) Own Pre Key Id + * 4) Their Key Group Id + * 5) Their Pre Key Id + *

+ * During actor starting it downloads all required key from Key Manager. + * To encrypt/decrypt messages this actor spawns encryption chains. + */ +class EncryptedSessionActor extends ModuleActor { + + private final String TAG; + + // No need to keep too much decryption chains as all messages are sequenced. Newer messages + // never intentionally use old keys, but there are cases when some messages can be sent with + // old encryption keys right after messages with new one. Even when we will kill sequence + // new actors can be easily started again with same keys. + // TODO: Check if this can cause race condition + private final int MAX_DECRYPT_CHAINS = 2; + + // + // Key References + // + + private final int uid; + private final PeerSession session; + + // + // Convenience references + // + + private KeyManager keyManager; + private KeyValueStorage sessionStorage; + + // + // Internal State + // + + private SessionState sessionState; + private EncryptedSessionChain encryptionChain = null; + private ArrayList decryptionChains = new ArrayList<>(); + private boolean isFreezed = false; + + // + // Constructors and Methods + // + + public EncryptedSessionActor(ModuleContext context, PeerSession session) { + super(context); + this.TAG = "EncryptionSessionActor#" + session.getUid() + "_" + session.getTheirKeyGroupId(); + this.uid = session.getUid(); + this.session = session; + } + + @Override + public void preStart() { + super.preStart(); + keyManager = context().getEncryption().getKeyManager(); + sessionStorage = context().getEncryption().getKeyValueStorage(); + + sessionState = new SessionState(); + byte[] data = sessionStorage.loadItem(session.getSid()); + if (data != null) { + try { + sessionState = SessionState.fromBytes(data); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + private void saveState() { + sessionStorage.addOrUpdateItem(session.getSid(), sessionState.toByteArray()); + } + + // + // Encryption + // + + private Promise onEncrypt(final byte[] data) { + + // Stage 2: Pick Encryption Chain + // Stage 3: Decrypt + + // + // Stage 1: Pick Their Ephemeral key + // - After this stage we will have public their key and own private key + // for encryption chain + // + Promise ephemeralKey; + if (sessionState.getLatestTheirKey() != null) { + ephemeralKey = success(sessionState.getLatestTheirKey()); + } else { + ephemeralKey = keyManager.getUserRandomPreKey(uid, session.getTheirKeyGroupId()).map(key -> { + // This can be called only for the first time of sending message in this session + // So we need to save ephemeral key and generate new initial private key + // for this session + sessionState = sessionState.updateKeys(key.getPublicKey()); + saveState(); + return key.getPublicKey(); + }); + } + + return wrap(ephemeralKey + .map(publicKey -> pickEncryptChain()) + .map(encryptedSessionChain -> encrypt(encryptedSessionChain, data)) + .map(bytes -> new ApiEncyptedBoxKey(session.getUid(), session.getTheirKeyGroupId(), + "curve25519", bytes))); + } + + private EncryptedSessionChain pickEncryptChain() { + + // Dispose existing encryption chain if their public keys changes + // If not return latest one + if (encryptionChain != null) { + if (ByteStrings.isEquals(sessionState.getLatestTheirKey(), encryptionChain.getTheirPublicKey()) && + ByteStrings.isEquals(sessionState.getLatestOwnPublicKey(), encryptionChain.getOwnPublicKey())) { + return encryptionChain; + } else { + encryptionChain = null; + } + } + + encryptionChain = new EncryptedSessionChain(session, + sessionState.getLatestOwnPrivateKey(), + sessionState.getLatestOwnPublicKey(), + sessionState.getLatestTheirKey()); + + return encryptionChain; + } + + private byte[] encrypt(EncryptedSessionChain chain, byte[] data) { + byte[] encrypted; + try { + encrypted = chain.encrypt(data); + } catch (IntegrityException e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + return encrypted; + } + + // + // Decryption + // + + private Promise onDecrypt(ApiEncyptedBoxKey data) { + + // + // Stage 1: Parsing message header + // Stage 2: Picking decryption chain + // Stage 3: Decryption of message + // Stage 4: Saving their ephemeral key + // + + byte[] material = data.getEncryptedKey(); + byte[] senderEphemeralKey = ByteStrings.substring(material, 16, 32); + byte[] receiverEphemeralKey = ByteStrings.substring(material, 48, 32); + + return wrap(pickDecryptChain(senderEphemeralKey, receiverEphemeralKey) + .map(encryptedSessionChain -> decrypt(encryptedSessionChain, material)) + .then(r -> { + // Update Session State keys + if (sessionState.getLatestTheirKey() == null || + ByteStrings.isEquals(sessionState.getLatestTheirKey(), senderEphemeralKey)) { + sessionState = sessionState.updateKeys(senderEphemeralKey); + saveState(); + } + })); + } + + + private Promise pickDecryptChain(final byte[] theirEphemeralKey, final byte[] ephemeralKey) { + EncryptedSessionChain pickedChain = null; + for (EncryptedSessionChain c : decryptionChains) { + if (ByteStrings.isEquals(c.getOwnPublicKey(), ephemeralKey)) { + pickedChain = c; + break; + } + } + if (pickedChain != null) { + return Promise.success(pickedChain); + } + + return findOwnPreKey(ephemeralKey).map(privateKey -> { + EncryptedSessionChain chain = new EncryptedSessionChain(session, privateKey, + ephemeralKey, theirEphemeralKey); + decryptionChains.add(0, chain); + if (decryptionChains.size() > MAX_DECRYPT_CHAINS) { + decryptionChains.remove(MAX_DECRYPT_CHAINS) + .safeErase(); + } + return chain; + }); + } + + private Promise findOwnPreKey(byte[] ephemeralKey) { + if (ByteStrings.isEquals(ephemeralKey, sessionState.getLatestOwnPublicKey())) { + return Promise.success(sessionState.getLatestOwnPrivateKey()); + } + if (ByteStrings.isEquals(ephemeralKey, sessionState.getPrevOwnPublicKey())) { + return Promise.success(sessionState.getPrevOwnPrivateKey()); + } + return context().getEncryption().getKeyManager().getOwnPreKey(ephemeralKey) + .map(PrivateKey::getKey); + } + + private byte[] decrypt(EncryptedSessionChain chain, byte[] data) { + byte[] decrypted; + try { + decrypted = chain.decrypt(data); + } catch (IntegrityException e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + return decrypted; + } + + // + // Tools + // + + private Promise wrap(Promise promise) { + isFreezed = true; + return promise.after((r, e) -> { + isFreezed = false; + unstashAll(); + }); + } + + // + // Actor Messages + // + + @Override + public Promise onAsk(Object message) throws Exception { + if (message instanceof EncryptPackage) { + if (isFreezed) { + stash(); + return null; + } + return onEncrypt(((EncryptPackage) message).getData()); + } else if (message instanceof DecryptPackage) { + if (isFreezed) { + stash(); + return null; + } + DecryptPackage decryptPackage = (DecryptPackage) message; + return onDecrypt(decryptPackage.getData()); + } else { + return super.onAsk(message); + } + } + + public static class EncryptPackage implements AskMessage { + private byte[] data; + + public EncryptPackage(byte[] data) { + this.data = data; + } + + public byte[] getData() { + return data; + } + } + + public static class DecryptPackage implements AskMessage { + + private ApiEncyptedBoxKey data; + + public DecryptPackage(ApiEncyptedBoxKey data) { + this.data = data; + } + + public ApiEncyptedBoxKey getData() { + return data; + } + } +} \ No newline at end of file diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/session/EncryptedSessionChain.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/EncryptedSessionChain.java similarity index 69% rename from actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/session/EncryptedSessionChain.java rename to actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/EncryptedSessionChain.java index 738a0502ba..fb5478f071 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/session/EncryptedSessionChain.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/EncryptedSessionChain.java @@ -1,12 +1,8 @@ -package im.actor.core.modules.encryption.session; - -import java.util.HashSet; -import java.util.Random; +package im.actor.core.modules.encryption.ratchet; import im.actor.core.entity.encryption.PeerSession; import im.actor.core.util.RandomUtils; import im.actor.runtime.Crypto; -import im.actor.runtime.Log; import im.actor.runtime.crypto.Curve25519; import im.actor.runtime.crypto.IntegrityException; import im.actor.runtime.crypto.box.ActorBox; @@ -21,16 +17,16 @@ public class EncryptedSessionChain { private PeerSession session; private byte[] ownPrivateKey; + private byte[] ownPublicKey; private byte[] theirPublicKey; - private HashSet receivedCounters; private int sentCounter; private byte[] rootChainKey; - public EncryptedSessionChain(PeerSession session, byte[] ownPrivateKey, byte[] theirPublicKey) { + public EncryptedSessionChain(PeerSession session, byte[] ownPrivateKey, byte[] ownPublicKey, byte[] theirPublicKey) { this.session = session; this.ownPrivateKey = ownPrivateKey; + this.ownPublicKey = ownPublicKey; this.theirPublicKey = theirPublicKey; - this.receivedCounters = new HashSet<>(); this.sentCounter = 0; this.rootChainKey = RatchetRootChainKey.makeRootChainKey( new RatchetPrivateKey(ownPrivateKey), @@ -50,9 +46,13 @@ public byte[] getTheirPublicKey() { return theirPublicKey; } + public byte[] getOwnPublicKey() { + return ownPublicKey; + } + public byte[] decrypt(byte[] data) throws IntegrityException { - if (data.length < 88) { + if (data.length < 80) { throw new IntegrityException("Data length is too small"); } @@ -60,40 +60,36 @@ public byte[] decrypt(byte[] data) throws IntegrityException { // Parsing message header // - final int senderKeyGroupId = ByteStrings.bytesToInt(data, 0); - final long senderEphermalKey0Id = ByteStrings.bytesToLong(data, 4); - final long receiverEphermalKey0Id = ByteStrings.bytesToLong(data, 12); - final byte[] senderEphemeralKey = ByteStrings.substring(data, 20, 32); - final byte[] receiverEphemeralKey = ByteStrings.substring(data, 52, 32); - final int messageIndex = ByteStrings.bytesToInt(data, 84); + final long senderEphermalKey0Id = ByteStrings.bytesToLong(data, 0); + final long receiverEphermalKey0Id = ByteStrings.bytesToLong(data, 8); + final byte[] senderEphemeralKey = ByteStrings.substring(data, 16, 32); + final byte[] receiverEphemeralKey = ByteStrings.substring(data, 48, 32); + final int messageIndex = ByteStrings.bytesToInt(data, 80); // // Validating header // -// if (senderKeyGroupId != session.getPeerKeyGroupId()) { -// throw new IntegrityException("Incorrect sender key group id"); -// } -// if (senderEphermalKey0Id != session.getTheirPreKey().getKeyId()) { -// throw new IntegrityException("Incorrect sender pre key id"); -// } -// if (receiverEphermalKey0Id != session.getOwnPreKey().getKeyId()) { -// throw new IntegrityException("Incorrect receiver pre key id"); -// } -// if (ByteStrings.isEquals(senderEphemeralKey, theirPublicKey)) { -// throw new IntegrityException("Incorrect sender ephemeral key"); -// } -// if (ByteStrings.isEquals(receiverEphemeralKey, ownPrivateKey)) { -// throw new IntegrityException("Incorrect receiver ephemeral key"); -// } + if (senderEphermalKey0Id != session.getTheirPreKeyId()) { + throw new IntegrityException("Incorrect sender pre key id"); + } + if (receiverEphermalKey0Id != session.getOwnPreKeyId()) { + throw new IntegrityException("Incorrect receiver pre key id"); + } + if (!ByteStrings.isEquals(senderEphemeralKey, theirPublicKey)) { + throw new IntegrityException("Incorrect sender ephemeral key"); + } + if (!ByteStrings.isEquals(receiverEphemeralKey, ownPublicKey)) { + throw new IntegrityException("Incorrect receiver ephemeral key"); + } // // Decryption // ActorBoxKey ratchetMessageKey = RatchetMessageKey.buildKey(rootChainKey, messageIndex); - byte[] header = ByteStrings.substring(data, 0, 88); - byte[] message = ByteStrings.substring(data, 88, data.length - 88); + byte[] header = ByteStrings.substring(data, 0, 84); + byte[] message = ByteStrings.substring(data, 84, data.length - 84); return ActorBox.openBox(header, message, ratchetMessageKey); } @@ -102,10 +98,9 @@ public byte[] encrypt(byte[] data) throws IntegrityException { ActorBoxKey ratchetMessageKey = RatchetMessageKey.buildKey(rootChainKey, messageIndex); byte[] header = ByteStrings.merge( - ByteStrings.intToBytes(session.getOwnKeyGroupId()), ByteStrings.longToBytes(session.getOwnPreKeyId()), /*Alice Initial Ephermal*/ ByteStrings.longToBytes(session.getTheirPreKeyId()), /*Bob Initial Ephermal*/ - Curve25519.keyGenPublic(ownPrivateKey), + ownPublicKey, theirPublicKey, ByteStrings.intToBytes(messageIndex)); /* Message Index */ @@ -125,7 +120,6 @@ public void safeErase() { for (int i = 0; i < rootChainKey.length; i++) { rootChainKey[i] = (byte) RandomUtils.randomId(255); } - receivedCounters.clear(); sentCounter = 0; } } \ No newline at end of file diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/EncryptedUser.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/EncryptedUser.java new file mode 100644 index 0000000000..8cbf6f4cbd --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/EncryptedUser.java @@ -0,0 +1,52 @@ +package im.actor.core.modules.encryption.ratchet; + +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +import im.actor.core.api.ApiEncyptedBoxKey; +import im.actor.core.modules.encryption.ratchet.entity.EncryptedUserKeys; +import im.actor.core.modules.encryption.ratchet.entity.UserKeys; +import im.actor.runtime.actors.ActorInterface; +import im.actor.runtime.actors.ActorRef; +import im.actor.runtime.promise.Promise; + +/** + * Encrypting shared key for private Secret Chats + */ +public class EncryptedUser extends ActorInterface { + + public EncryptedUser(@NotNull ActorRef dest) { + super(dest); + } + + /** + * Encrypting shared key + * + * @param data shared key for encryption + * @return promise of list of encrypted shared key + */ + public Promise encrypt(byte[] data) { + return ask(new EncryptedUserActor.EncryptBox(data)); + } + + /** + * Decrypting shared key + * + * @param senderKeyGroupId sender's key group id + * @param keys list of encrypted box keys + * @return promise of shared key + */ + public Promise decrypt(int senderKeyGroupId, List keys) { + return ask(new EncryptedUserActor.DecryptBox(senderKeyGroupId, keys)); + } + + /** + * Notify about user keys updated for refreshing internal keys cache + * + * @param updatedUserKeys updated user keys + */ + public void onUserKeysChanged(UserKeys updatedUserKeys) { + send(new EncryptedUserActor.KeyGroupUpdated(updatedUserKeys)); + } +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/EncryptedUserActor.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/EncryptedUserActor.java new file mode 100644 index 0000000000..afe391dcc1 --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/EncryptedUserActor.java @@ -0,0 +1,275 @@ +package im.actor.core.modules.encryption.ratchet; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; + +import im.actor.core.api.ApiEncyptedBoxKey; +import im.actor.core.entity.encryption.PeerSession; +import im.actor.core.modules.ModuleContext; +import im.actor.core.modules.encryption.ratchet.entity.EncryptedUserKeys; +import im.actor.core.modules.encryption.ratchet.entity.UserKeys; +import im.actor.core.modules.ModuleActor; +import im.actor.runtime.*; +import im.actor.runtime.actors.ask.AskMessage; +import im.actor.runtime.promise.Promise; +import im.actor.runtime.crypto.primitives.util.ByteStrings; +import im.actor.runtime.promise.Promises; +import im.actor.runtime.promise.PromisesArray; + +import static im.actor.runtime.promise.Promise.success; + +public class EncryptedUserActor extends ModuleActor { + + private final String TAG; + + private final int uid; + private final boolean isOwnUser; + + private int ownKeyGroupId; + private UserKeys theirKeys; + + private HashMap activeSessions = new HashMap<>(); + private HashSet ignoredKeyGroups = new HashSet<>(); + + private boolean isFreezed = true; + + public EncryptedUserActor(int uid, ModuleContext context) { + super(context); + this.uid = uid; + this.isOwnUser = myUid() == uid; + TAG = "EncryptedUserActor#" + uid; + } + + @Override + public void preStart() { + super.preStart(); + + KeyManager keyManager = context().getEncryption().getKeyManager(); + + Promises.tuple( + keyManager.getOwnIdentity(), + keyManager.getUserKeyGroups(uid)) + .then(res -> { + Log.d(TAG, "Loaded initial parameters"); + ownKeyGroupId = res.getT1().getKeyGroup(); + theirKeys = res.getT2(); + if (isOwnUser) { + ignoredKeyGroups.add(ownKeyGroupId); + } + isFreezed = false; + unstashAll(); + }) + .failure(e -> { + Log.w(TAG, "Unable to fetch initial parameters. Freezing encryption with user #" + uid); + Log.e(TAG, e); + }); + } + + private Promise doEncrypt(byte[] data) { + + // Stage 1: Loading User Key Groups + return wrap(PromisesArray.of(theirKeys.getUserKeysGroups()) + + // Stage 1.1: Filtering invalid key groups and own key groups + .filter(keysGroup -> !ignoredKeyGroups.contains(keysGroup.getKeyGroupId()) + && (!(isOwnUser && keysGroup.getKeyGroupId() == ownKeyGroupId))) + + // Stage 2: Pick sessions for encryption + .map(keysGroup -> { + if (activeSessions.containsKey(keysGroup.getKeyGroupId())) { + return success(activeSessions.get(keysGroup.getKeyGroupId()).first()); + } + return getSessionManager() + .pickSession(uid, keysGroup.getKeyGroupId()) + .map(src -> spawnSession(src)) + .failure(e -> { + ignoredKeyGroups.add(keysGroup.getKeyGroupId()); + }); + }) + .filterFailed() + + // Stage 3: Encrypt box_keys + .map(s -> s.encrypt(data)) + .filterFailed() + + // Stage 4: Zip Everything together + .zip(src -> new EncryptedUserKeys(uid, src, new HashSet<>(ignoredKeyGroups)))); + } + + private Promise doDecrypt(int senderKeyGroupId, List keys) { + + // + // Picking key + // + if (ignoredKeyGroups.contains(senderKeyGroupId)) { + throw new RuntimeException("This key group is ignored"); + } + ApiEncyptedBoxKey key = null; + for (ApiEncyptedBoxKey boxKey : keys) { + if (boxKey.getKeyGroupId() == ownKeyGroupId && boxKey.getUsersId() == myUid()) { + key = boxKey; + break; + } + } + if (key == null) { + throw new RuntimeException("Unable to find suitable key group's key"); + } + final ApiEncyptedBoxKey finalKey = key; + + // + // Decryption + // + long senderPreKeyId = ByteStrings.bytesToLong(key.getEncryptedKey(), 0); + long receiverPreKeyId = ByteStrings.bytesToLong(key.getEncryptedKey(), 8); + if (activeSessions.containsKey(key.getKeyGroupId())) { + for (EncryptedSession s : activeSessions.get(senderKeyGroupId).getSessions()) { + if (s.getSession().getOwnPreKeyId() == receiverPreKeyId && + s.getSession().getTheirPreKeyId() == senderPreKeyId) { + return wrap(s.decrypt(key)); + } + } + } + return wrap(getSessionManager() + .pickSession(uid, senderKeyGroupId, receiverPreKeyId, senderPreKeyId) + .flatMap(src -> spawnSession(src).decrypt(finalKey))); + } + + private void onKeysUpdated(UserKeys userKeys) { + this.theirKeys = userKeys; + } + + + // + // Tools + // + + private EncryptedSession spawnSession(PeerSession peerSession) { + EncryptedSession session = new EncryptedSession(peerSession, context()); + if (activeSessions.containsKey(peerSession.getTheirKeyGroupId())) { + activeSessions.get(peerSession.getTheirKeyGroupId()).getSessions().add(session); + } else { + ArrayList l = new ArrayList<>(); + l.add(session); + activeSessions.put(peerSession.getTheirKeyGroupId(), new KeyGroupHolder(peerSession.getTheirKeyGroupId(), l)); + } + return session; + } + + private Promise wrap(Promise p) { + isFreezed = true; + p.after((r, e) -> { + isFreezed = false; + unstashAll(); + }); + return p; + } + + private SessionManager getSessionManager() { + return context().getEncryption().getSessionManager(); + } + + + // + // Messages + // + + @Override + public Promise onAsk(Object message) throws Exception { + if (isFreezed) { + stash(); + return null; + } + + if (message instanceof EncryptBox) { + EncryptBox encryptBox = (EncryptBox) message; + return doEncrypt(encryptBox.getData()); + } else if (message instanceof DecryptBox) { + DecryptBox decryptBox = (DecryptBox) message; + return doDecrypt(decryptBox.getSenderKeyGroupId(), decryptBox.getKeys()); + } else { + return super.onAsk(message); + } + } + + @Override + public void onReceive(Object message) { + if (message instanceof KeyGroupUpdated) { + if (isFreezed) { + stash(); + return; + } + onKeysUpdated(((KeyGroupUpdated) message).getUserKeys()); + } else { + super.onReceive(message); + } + } + + public static class EncryptBox implements AskMessage { + private byte[] data; + + public EncryptBox(byte[] data) { + this.data = data; + } + + public byte[] getData() { + return data; + } + } + + public static class DecryptBox implements AskMessage { + + private int senderKeyGroupId; + private List keys; + + public DecryptBox(int senderKeyGroupId, List keys) { + this.senderKeyGroupId = senderKeyGroupId; + this.keys = keys; + } + + public int getSenderKeyGroupId() { + return senderKeyGroupId; + } + + public List getKeys() { + return keys; + } + } + + public static class KeyGroupUpdated { + + private UserKeys userKeys; + + public KeyGroupUpdated(UserKeys userKeys) { + this.userKeys = userKeys; + } + + public UserKeys getUserKeys() { + return userKeys; + } + } + + private class KeyGroupHolder { + + private int keyGroupId; + private ArrayList sessions; + + public KeyGroupHolder(int keyGroupId, ArrayList sessions) { + this.keyGroupId = keyGroupId; + this.sessions = sessions; + } + + public int getKeyGroupId() { + return keyGroupId; + } + + public ArrayList getSessions() { + return sessions; + } + + public EncryptedSession first() { + return sessions.get(0); + } + } +} \ No newline at end of file diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/KeyManager.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/KeyManager.java new file mode 100644 index 0000000000..d48ed9d25d --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/KeyManager.java @@ -0,0 +1,150 @@ +package im.actor.core.modules.encryption.ratchet; + +import java.util.List; + +import im.actor.core.api.ApiEncryptionKeyGroup; +import im.actor.core.api.ApiKeyGroupHolder; +import im.actor.core.api.ApiKeyGroupId; +import im.actor.core.modules.ModuleContext; +import im.actor.core.modules.encryption.ratchet.entity.OwnIdentity; +import im.actor.core.modules.encryption.ratchet.entity.PrivateKey; +import im.actor.core.modules.encryption.ratchet.entity.PublicKey; +import im.actor.core.modules.encryption.ratchet.entity.UserKeys; +import im.actor.runtime.actors.ActorInterface; +import im.actor.runtime.actors.messages.Void; +import im.actor.runtime.promise.Promise; + +import static im.actor.runtime.actors.ActorSystem.system; + +/** + * Encryption Key Manager. Used for loading user's keys for encryption/decryption. + */ +public class KeyManager extends ActorInterface { + + /** + * Default Constructor + * + * @param context actor context + */ + public KeyManager(ModuleContext context) { + super(system().actorOf("encryption/keys", () -> new KeyManagerActor(context))); + } + + + // + // Identity + // + + /** + * Loading Own Identity Key + * + * @return promise of keys + */ + public Promise getOwnIdentity() { + return ask(new KeyManagerActor.FetchOwnKey()); + } + + /** + * Loading user key groups by uid + * + * @param uid user's id + * @return promise of key groups + */ + public Promise getUserKeyGroups(int uid) { + return ask(new KeyManagerActor.FetchUserKeys(uid)); + } + + + // + // Pre Keys + // + + /** + * Load own random pre key + * + * @return promise of private key + */ + public Promise getOwnRandomPreKey() { + return ask(new KeyManagerActor.FetchOwnRandomPreKey()); + } + + /** + * Load own pre key by key id + * + * @param id key id + * @return promise of private key + */ + public Promise getOwnPreKey(long id) { + return ask(new KeyManagerActor.FetchOwnPreKeyById(id)); + } + + /** + * Load own pre key by public key + * + * @param publicKey public key + * @return promise of private key + */ + public Promise getOwnPreKey(byte[] publicKey) { + return ask(new KeyManagerActor.FetchOwnPreKeyByPublic(publicKey)); + } + + /** + * Loading random user's pre key from key group + * + * @param uid user's id + * @param keyGroupId key group id + * @return promise of public key + */ + public Promise getUserRandomPreKey(int uid, int keyGroupId) { + return ask(new KeyManagerActor.FetchUserPreKeyRandom(uid, keyGroupId)); + } + + /** + * Loading user's pre key by pre key id + * + * @param uid user's id + * @param keyGroupId key group id + * @param preKeyId pre key id + * @return promise of public key + */ + public Promise getUserPreKey(int uid, int keyGroupId, long preKeyId) { + return ask(new KeyManagerActor.FetchUserPreKey(uid, keyGroupId, preKeyId)); + } + + // + // Updates + // + + /** + * Call this when update about new key group added received + * + * @param uid user's id + * @param keyGroup added key group + * @return promise of void + */ + public Promise onKeyGroupAdded(int uid, ApiEncryptionKeyGroup keyGroup) { + return ask(new KeyManagerActor.PublicKeysGroupAdded(uid, keyGroup)); + } + + /** + * Call this when update about key group removing received + * + * @param uid user's id + * @param gid removed key group id + * @return promise of void + */ + public Promise onKeyGroupRemoved(int uid, int gid) { + return ask(new KeyManagerActor.PublicKeysGroupRemoved(uid, gid)); + } + + /** + * Call this when you will receive error during encrytped message sending + * + * @param missed missed key groups + * @param obsolete obsolete key groups + * @return promise of void + */ + public Promise onKeyGroupDiffReceived(List missed, List obsolete) { + return ask(new KeyManagerActor.KeyGroupsDiff(missed, obsolete)); + } +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/KeyManagerActor.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/KeyManagerActor.java similarity index 61% rename from actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/KeyManagerActor.java rename to actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/KeyManagerActor.java index 9a6501c501..71520d36a6 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/KeyManagerActor.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/KeyManagerActor.java @@ -1,44 +1,45 @@ -package im.actor.core.modules.encryption; +package im.actor.core.modules.encryption.ratchet; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import im.actor.core.api.ApiEncryptionKey; import im.actor.core.api.ApiEncryptionKeyGroup; import im.actor.core.api.ApiEncryptionKeySignature; +import im.actor.core.api.ApiKeyGroupHolder; +import im.actor.core.api.ApiKeyGroupId; import im.actor.core.api.ApiUserOutPeer; import im.actor.core.api.rpc.RequestCreateNewKeyGroup; import im.actor.core.api.rpc.RequestLoadPrePublicKeys; import im.actor.core.api.rpc.RequestLoadPublicKey; import im.actor.core.api.rpc.RequestLoadPublicKeyGroups; import im.actor.core.api.rpc.RequestUploadPreKey; -import im.actor.core.api.rpc.ResponseCreateNewKeyGroup; -import im.actor.core.api.rpc.ResponsePublicKeyGroups; -import im.actor.core.api.rpc.ResponsePublicKeys; import im.actor.core.api.rpc.ResponseVoid; import im.actor.core.entity.User; import im.actor.core.modules.ModuleContext; -import im.actor.core.modules.encryption.entity.PrivateKeyStorage; -import im.actor.core.modules.encryption.entity.PrivateKey; -import im.actor.core.modules.encryption.entity.UserKeys; -import im.actor.core.modules.encryption.entity.UserKeysGroup; -import im.actor.core.modules.encryption.entity.PublicKey; +import im.actor.core.modules.encryption.Configuration; +import im.actor.core.modules.encryption.ratchet.entity.OwnIdentity; +import im.actor.core.modules.encryption.ratchet.entity.PrivateKeyStorage; +import im.actor.core.modules.encryption.ratchet.entity.PrivateKey; +import im.actor.core.modules.encryption.ratchet.entity.UserKeys; +import im.actor.core.modules.encryption.ratchet.entity.UserKeysGroup; +import im.actor.core.modules.encryption.ratchet.entity.PublicKey; import im.actor.core.modules.ModuleActor; import im.actor.core.util.RandomUtils; import im.actor.runtime.Crypto; import im.actor.runtime.Log; import im.actor.runtime.Storage; -import im.actor.runtime.actors.ask.AskIntRequest; import im.actor.runtime.actors.ask.AskMessage; -import im.actor.runtime.actors.ask.AskResult; +import im.actor.runtime.actors.messages.Void; import im.actor.runtime.collections.ManagedList; import im.actor.runtime.crypto.Curve25519KeyPair; -import im.actor.runtime.function.Function; import im.actor.runtime.promise.Promise; import im.actor.runtime.crypto.Curve25519; import im.actor.runtime.crypto.ratchet.RatchetKeySignature; -import im.actor.runtime.function.Consumer; +import im.actor.runtime.promise.PromiseTools; +import im.actor.runtime.promise.Promises; import im.actor.runtime.promise.PromisesArray; import im.actor.runtime.function.Tuple2; import im.actor.runtime.storage.KeyValueStorage; @@ -50,10 +51,14 @@ */ public class KeyManagerActor extends ModuleActor { + // j2objc workaround + private static final Void DUMB = null; + private static final ResponseVoid DUMB2 = null; + private static final String TAG = "KeyManagerActor"; private KeyValueStorage encryptionKeysStorage; - private HashMap cachedUserKeys = new HashMap(); + private HashMap cachedUserKeys = new HashMap<>(); private PrivateKeyStorage ownKeys; private boolean isReady = false; @@ -121,21 +126,15 @@ public void preStart() { .map(PrivateKey.SIGN(ownKeys.getIdentityKey())); Log.d(TAG, "Creation of new key group"); - api(new RequestCreateNewKeyGroup(identityKey, Configuration.SUPPORTED, keys, signatures)).then(new Consumer() { - @Override - public void apply(ResponseCreateNewKeyGroup response) { - ownKeys = ownKeys.setGroupId(response.getKeyGroupId()); - encryptionKeysStorage.addOrUpdateItem(0, ownKeys.toByteArray()); - onMainKeysReady(); - } - }).failure(new Consumer() { - @Override - public void apply(Exception e) { - Log.w(TAG, "Keys upload error"); - Log.e(TAG, e); - - // Just ignore - } + api(new RequestCreateNewKeyGroup(identityKey, Configuration.SUPPORTED, keys, signatures)).then(response -> { + ownKeys = ownKeys.setGroupId(response.getKeyGroupId()); + encryptionKeysStorage.addOrUpdateItem(0, ownKeys.toByteArray()); + onMainKeysReady(); + }).failure(e -> { + Log.w(TAG, "Keys upload error"); + Log.e(TAG, e); + + // Just ignore }); } else { onMainKeysReady(); @@ -173,22 +172,16 @@ private void onMainKeysReady() { pendingEphermalKeys.map(PrivateKey.SIGN(ownKeys.getIdentityKey())); api(new RequestUploadPreKey(ownKeys.getKeyGroupId(), uploadingKeys, uploadingSignatures)) - .then(new Consumer() { - @Override - public void apply(ResponseVoid responseVoid) { - ownKeys = ownKeys.markAsUploaded(pendingEphermalKeys.toArray(new PrivateKey[pendingEphermalKeys.size()])); - encryptionKeysStorage.addOrUpdateItem(0, ownKeys.toByteArray()); - onAllKeysReady(); - } + .then(responseVoid -> { + ownKeys = ownKeys.markAsUploaded(pendingEphermalKeys.toArray(new PrivateKey[pendingEphermalKeys.size()])); + encryptionKeysStorage.addOrUpdateItem(0, ownKeys.toByteArray()); + onAllKeysReady(); }) - .failure(new Consumer() { - @Override - public void apply(Exception e) { - Log.w(TAG, "Ephemeral keys upload error"); - Log.e(TAG, e); + .failure(e -> { + Log.w(TAG, "Ephemeral keys upload error"); + Log.e(TAG, e); - // Ignore. This will freeze all encryption operations. - } + // Ignore. This will freeze all encryption operations. }); } else { onAllKeysReady(); @@ -282,26 +275,20 @@ private Promise fetchUserGroups(final int uid) { } return api(new RequestLoadPublicKeyGroups(new ApiUserOutPeer(uid, user.getAccessHash()))) - .map(new Function>() { - @Override - public ArrayList apply(ResponsePublicKeyGroups response) { - ArrayList keysGroups = new ArrayList<>(); - for (ApiEncryptionKeyGroup keyGroup : response.getPublicKeyGroups()) { - UserKeysGroup validatedKeysGroup = validateUserKeysGroup(uid, keyGroup); - if (validatedKeysGroup != null) { - keysGroups.add(validatedKeysGroup); - } + .map(response -> { + ArrayList keysGroups = new ArrayList<>(); + for (ApiEncryptionKeyGroup keyGroup : response.getPublicKeyGroups()) { + UserKeysGroup validatedKeysGroup = validateUserKeysGroup(uid, keyGroup); + if (validatedKeysGroup != null) { + keysGroups.add(validatedKeysGroup); } - return keysGroups; } + return keysGroups; }) - .map(new Function, UserKeys>() { - @Override - public UserKeys apply(ArrayList userKeysGroups) { - UserKeys userKeys = new UserKeys(uid, userKeysGroups.toArray(new UserKeysGroup[userKeysGroups.size()])); - cacheUserKeys(userKeys); - return userKeys; - } + .map(userKeysGroups -> { + UserKeys userKeys1 = new UserKeys(uid, userKeysGroups.toArray(new UserKeysGroup[userKeysGroups.size()])); + cacheUserKeys(userKeys1); + return userKeys1; }); } @@ -320,65 +307,62 @@ private Promise fetchUserPreKey(final int uid, final int keyGroupId, } return pickUserGroup(uid, keyGroupId) - .flatMap(new Function, Promise>() { - @Override - public Promise apply(final Tuple2 keysGroup) { - - // - // Searching in cache - // - - for (PublicKey p : keysGroup.getT1().getEphemeralKeys()) { - if (p.getKeyId() == keyId) { - return Promise.success(p); - } + .flatMap(keysGroup -> { + + // + // Searching in cache + // + + for (PublicKey p : keysGroup.getT1().getEphemeralKeys()) { + if (p.getKeyId() == keyId) { + return Promise.success(p); } + } - // - // Downloading pre key - // - - ArrayList ids = new ArrayList(); - ids.add(keyId); - final UserKeysGroup finalKeysGroup = keysGroup.getT1(); - - return api(new RequestLoadPublicKey(new ApiUserOutPeer(uid, getUser(uid).getAccessHash()), keyGroupId, ids)) - .map(new Function() { - @Override - public PublicKey apply(ResponsePublicKeys responsePublicKeys) { - if (responsePublicKeys.getPublicKey().size() == 0) { - throw new RuntimeException("Unable to find public key on server"); - } - ApiEncryptionKeySignature sig = null; - for (ApiEncryptionKeySignature s : responsePublicKeys.getSignatures()) { - if (s.getKeyId() == keyId && "Ed25519".equals(s.getSignatureAlg())) { - sig = s; - break; - } - } - if (sig == null) { - throw new RuntimeException("Unable to find public key on server"); - } - - ApiEncryptionKey key = responsePublicKeys.getPublicKey().get(0); - - byte[] keyHash = RatchetKeySignature.hashForSignature(key.getKeyId(), key.getKeyAlg(), - key.getKeyMaterial()); - - if (!Curve25519.verifySignature(keysGroup.getT1().getIdentityKey().getPublicKey(), - keyHash, sig.getSignature())) { - throw new RuntimeException("Key signature does not isMatch"); - } - - PublicKey pkey = new PublicKey(keyId, key.getKeyAlg(), key.getKeyMaterial()); - UserKeysGroup userKeysGroup = finalKeysGroup.addPublicKey(pkey); - cacheUserKeys(keysGroup.getT2().removeUserKeyGroup(userKeysGroup.getKeyGroupId()) - .addUserKeyGroup(userKeysGroup)); - - return pkey; + // + // Downloading pre key + // + + ArrayList ids = new ArrayList<>(); + ids.add(keyId); + final UserKeysGroup finalKeysGroup = keysGroup.getT1(); + RequestLoadPublicKey request = new RequestLoadPublicKey( + new ApiUserOutPeer(uid, getUser(uid).getAccessHash()), + keyGroupId, ids); + + return api(request) + .map(responsePublicKeys -> { + if (responsePublicKeys.getPublicKey().size() == 0) { + throw new RuntimeException("Unable to find public key on server"); + } + ApiEncryptionKeySignature sig = null; + for (ApiEncryptionKeySignature s : responsePublicKeys.getSignatures()) { + if (s.getKeyId() == keyId && "Ed25519".equals(s.getSignatureAlg())) { + sig = s; + break; } - }); - } + } + if (sig == null) { + throw new RuntimeException("Unable to find public key on server"); + } + + ApiEncryptionKey key = responsePublicKeys.getPublicKey().get(0); + + byte[] keyHash = RatchetKeySignature.hashForSignature(key.getKeyId(), key.getKeyAlg(), + key.getKeyMaterial()); + + if (!Curve25519.verifySignature(keysGroup.getT1().getIdentityKey().getPublicKey(), + keyHash, sig.getSignature())) { + throw new RuntimeException("Key signature does not match"); + } + + PublicKey pkey = new PublicKey(keyId, key.getKeyAlg(), key.getKeyMaterial()); + UserKeysGroup userKeysGroup = finalKeysGroup.addPublicKey(pkey); + cacheUserKeys(keysGroup.getT2().removeUserKeyGroup(userKeysGroup.getKeyGroupId()) + .addUserKeyGroup(userKeysGroup)); + + return pkey; + }); }); } @@ -390,40 +374,34 @@ public PublicKey apply(ResponsePublicKeys responsePublicKeys) { */ private Promise fetchUserPreKey(final int uid, final int keyGroupId) { return pickUserGroup(uid, keyGroupId) - .flatMap(new Function, Promise>() { - @Override - public Promise apply(final Tuple2 keyGroups) { - return api(new RequestLoadPrePublicKeys(new ApiUserOutPeer(uid, getUser(uid).getAccessHash()), keyGroupId)) - .map(new Function() { - @Override - public PublicKey apply(ResponsePublicKeys response) { - if (response.getPublicKey().size() == 0) { - throw new RuntimeException("User doesn't have pre keys"); - } - ApiEncryptionKey key = response.getPublicKey().get(0); - ApiEncryptionKeySignature sig = null; - for (ApiEncryptionKeySignature s : response.getSignatures()) { - if (s.getKeyId() == key.getKeyId() && "Ed25519".equals(s.getSignatureAlg())) { - sig = s; - break; - } - } - if (sig == null) { - throw new RuntimeException("Unable to find public key on server"); - } - - byte[] keyHash = RatchetKeySignature.hashForSignature(key.getKeyId(), key.getKeyAlg(), - key.getKeyMaterial()); - - if (!Curve25519.verifySignature(keyGroups.getT1().getIdentityKey().getPublicKey(), - keyHash, sig.getSignature())) { - throw new RuntimeException("Key signature does not isMatch"); - } - - return new PublicKey(key.getKeyId(), key.getKeyAlg(), key.getKeyMaterial()); + .flatMap(keyGroups -> { + return api(new RequestLoadPrePublicKeys(new ApiUserOutPeer(uid, getUser(uid).getAccessHash()), keyGroupId)) + .map(response -> { + if (response.getPublicKey().size() == 0) { + throw new RuntimeException("User doesn't have pre keys"); + } + ApiEncryptionKey key = response.getPublicKey().get(0); + ApiEncryptionKeySignature sig = null; + for (ApiEncryptionKeySignature s : response.getSignatures()) { + if (s.getKeyId() == key.getKeyId() && "Ed25519".equals(s.getSignatureAlg())) { + sig = s; + break; } - }); - } + } + if (sig == null) { + throw new RuntimeException("Unable to find public key on server"); + } + + byte[] keyHash = RatchetKeySignature.hashForSignature(key.getKeyId(), key.getKeyAlg(), + key.getKeyMaterial()); + + if (!Curve25519.verifySignature(keyGroups.getT1().getIdentityKey().getPublicKey(), + keyHash, sig.getSignature())) { + throw new RuntimeException("Key signature does not match"); + } + + return new PublicKey(key.getKeyId(), key.getKeyAlg(), key.getKeyMaterial()); + }); }); } @@ -437,18 +415,20 @@ public PublicKey apply(ResponsePublicKeys response) { * @param uid User's id * @param keyGroup Added key group */ - private void onPublicKeysGroupAdded(int uid, ApiEncryptionKeyGroup keyGroup) { + private Promise onPublicKeysGroupAdded(int uid, ApiEncryptionKeyGroup keyGroup) { UserKeys userKeys = getCachedUserKeys(uid); if (userKeys == null) { - return; + return Promise.success(null); } UserKeysGroup validatedKeysGroup = validateUserKeysGroup(uid, keyGroup); if (validatedKeysGroup != null) { UserKeys updatedUserKeys = userKeys.addUserKeyGroup(validatedKeysGroup); cacheUserKeys(updatedUserKeys); - context().getEncryption().getEncryptedChatManager(uid) - .send(new EncryptedPeerActor.KeyGroupUpdated(userKeys)); + context().getEncryption() + .getEncryptedUser(uid) + .onUserKeysChanged(updatedUserKeys); } + return Promise.success(null); } /** @@ -457,16 +437,40 @@ private void onPublicKeysGroupAdded(int uid, ApiEncryptionKeyGroup keyGroup) { * @param uid User's id * @param keyGroupId Removed key group id */ - private void onPublicKeysGroupRemoved(int uid, int keyGroupId) { + private Promise onPublicKeysGroupRemoved(int uid, int keyGroupId) { UserKeys userKeys = getCachedUserKeys(uid); if (userKeys == null) { - return; + return Promise.success(null); } UserKeys updatedUserKeys = userKeys.removeUserKeyGroup(keyGroupId); cacheUserKeys(updatedUserKeys); - context().getEncryption().getEncryptedChatManager(uid) - .send(new EncryptedPeerActor.KeyGroupUpdated(userKeys)); + context().getEncryption() + .getEncryptedUser(uid) + .onUserKeysChanged(updatedUserKeys); + return Promise.success(null); + } + + + /** + * Handling response of incorrect keys + * + * @param missed missed key groups + * @param obsolete obsolete key group id + * @return promise of void + */ + private Promise onKeysDiffReceived(List missed, List obsolete) { + ArrayList> res = new ArrayList<>(); + + for (ApiKeyGroupHolder gh : missed) { + res.add(onPublicKeysGroupAdded(gh.getUid(), gh.getKeyGroup())); + } + for (ApiKeyGroupId gi : obsolete) { + res.add(onPublicKeysGroupRemoved(gi.getUid(), gi.getKeyGroupId())); + } + + return PromisesArray.ofPromises(res) + .zip(r -> null); } // @@ -485,7 +489,7 @@ private UserKeysGroup validateUserKeysGroup(int uid, ApiEncryptionKeyGroup keyGr keyGroup.getIdentityKey().getKeyAlg(), keyGroup.getIdentityKey().getKeyMaterial()); - ArrayList keys = new ArrayList(); + ArrayList keys = new ArrayList<>(); key_loop: for (ApiEncryptionKey key : keyGroup.getKeys()) { @@ -536,16 +540,15 @@ private Promise> pickUserGroup(int uid, final in return fetchUserGroups(uid) .map(userKeys -> { UserKeysGroup keysGroup = null; -// for (UserKeysGroup g : userKeys.getUserKeysGroups()) { -// if (g.getKeyGroupId() == keyGroupId) { -// keysGroup = g; -// } -// } -// if (keysGroup == null) { -// throw new RuntimeException("Key Group #" + keyGroupId + " not found"); -// } -// return new Tuple2<>(keysGroup, userKeys); - return null; + for (UserKeysGroup g : userKeys.getUserKeysGroups()) { + if (g.getKeyGroupId() == keyGroupId) { + keysGroup = g; + } + } + if (keysGroup == null) { + throw new RuntimeException("Key Group #" + keyGroupId + " not found"); + } + return new Tuple2<>(keysGroup, userKeys); }); } @@ -574,27 +577,12 @@ private void cacheUserKeys(UserKeys userKeys) { // @Override - public void onReceive(Object message) { - if (!isReady - && (message instanceof AskIntRequest - || message instanceof PublicKeysGroupAdded - || message instanceof PublicKeysGroupRemoved)) { + public Promise onAsk(Object message) throws Exception { + if (!isReady) { stash(); - return; - } - if (message instanceof PublicKeysGroupAdded) { - PublicKeysGroupAdded publicKeysGroupAdded = (PublicKeysGroupAdded) message; - onPublicKeysGroupAdded(publicKeysGroupAdded.getUid(), publicKeysGroupAdded.getPublicKeyGroup()); - } else if (message instanceof PublicKeysGroupRemoved) { - PublicKeysGroupRemoved publicKeysGroupRemoved = (PublicKeysGroupRemoved) message; - onPublicKeysGroupRemoved(publicKeysGroupRemoved.getUid(), publicKeysGroupRemoved.getKeyGroupId()); - } else { - super.onReceive(message); + return null; } - } - @Override - public Promise onAsk(Object message) throws Exception { if (message instanceof FetchOwnKey) { return fetchOwnIdentity(); } else if (message instanceof FetchOwnPreKeyByPublic) { @@ -609,6 +597,15 @@ public Promise onAsk(Object message) throws Exception { return fetchUserPreKey(((FetchUserPreKeyRandom) message).getUid(), ((FetchUserPreKeyRandom) message).getKeyGroup()); } else if (message instanceof FetchOwnRandomPreKey) { return fetchPreKey(); + } else if (message instanceof PublicKeysGroupAdded) { + PublicKeysGroupAdded publicKeysGroupAdded = (PublicKeysGroupAdded) message; + return onPublicKeysGroupAdded(publicKeysGroupAdded.getUid(), publicKeysGroupAdded.getPublicKeyGroup()); + } else if (message instanceof PublicKeysGroupRemoved) { + PublicKeysGroupRemoved publicKeysGroupRemoved = (PublicKeysGroupRemoved) message; + return onPublicKeysGroupRemoved(publicKeysGroupRemoved.getUid(), publicKeysGroupRemoved.getKeyGroupId()); + } else if (message instanceof KeyGroupsDiff) { + KeyGroupsDiff diff = (KeyGroupsDiff) message; + return onKeysDiffReceived(diff.getMissed(), diff.getObsolete()); } else { return super.onAsk(message); } @@ -622,25 +619,6 @@ public static class FetchOwnKey implements AskMessage { } - public static class OwnIdentity extends AskResult { - - private int keyGroup; - private PrivateKey identityKey; - - public OwnIdentity(int keyGroup, PrivateKey identityKey) { - this.keyGroup = keyGroup; - this.identityKey = identityKey; - } - - public int getKeyGroup() { - return keyGroup; - } - - public PrivateKey getIdentityKey() { - return identityKey; - } - } - public static class FetchOwnRandomPreKey implements AskMessage { } @@ -735,7 +713,7 @@ public int getKeyGroup() { // Updates handling // - public static class PublicKeysGroupAdded { + public static class PublicKeysGroupAdded implements AskMessage { private int uid; private ApiEncryptionKeyGroup publicKeyGroup; @@ -754,7 +732,7 @@ public ApiEncryptionKeyGroup getPublicKeyGroup() { } } - public static class PublicKeysGroupRemoved { + public static class PublicKeysGroupRemoved implements AskMessage { private int uid; private int keyGroupId; @@ -772,4 +750,27 @@ public int getKeyGroupId() { return keyGroupId; } } + + // + // Missed or wrong key groups + // + + public static class KeyGroupsDiff implements AskMessage { + + private List missed; + private List obsolete; + + public KeyGroupsDiff(List missed, List obsolete) { + this.missed = missed; + this.obsolete = obsolete; + } + + public List getMissed() { + return missed; + } + + public List getObsolete() { + return obsolete; + } + } } \ No newline at end of file diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/SessionManager.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/SessionManager.java new file mode 100644 index 0000000000..90371d77fe --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/SessionManager.java @@ -0,0 +1,44 @@ +package im.actor.core.modules.encryption.ratchet; + +import im.actor.core.entity.encryption.PeerSession; +import im.actor.core.modules.ModuleContext; +import im.actor.runtime.actors.ActorInterface; +import im.actor.runtime.promise.Promise; + +import static im.actor.runtime.actors.ActorSystem.system; + +/** + * Session Manager for encrypted chats. + * Stores and manages encrypted sessions between users. + * Can be asked to pick session parameters for specific users. + */ +public class SessionManager extends ActorInterface { + + public SessionManager(ModuleContext context) { + super(system().actorOf("encryption/sessions", () -> new SessionManagerActor(context))); + } + + /** + * Pick fresh session with random pre keys + * + * @param uid user's id + * @param keyGroup key group id + * @return promise of session + */ + public Promise pickSession(int uid, int keyGroup) { + return ask(new SessionManagerActor.PickSessionForEncrypt(uid, keyGroup)); + } + + /** + * Pick session with specific identity keys + * + * @param uid user's id + * @param keyGroup key group id + * @param ownKeyId own identity prekey id + * @param theirKeyId their identity prekey id + * @return promise of session + */ + public Promise pickSession(int uid, int keyGroup, long ownKeyId, long theirKeyId) { + return ask(new SessionManagerActor.PickSessionForDecrypt(uid, keyGroup, theirKeyId, ownKeyId)); + } +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/SessionManagerActor.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/SessionManagerActor.java similarity index 59% rename from actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/SessionManagerActor.java rename to actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/SessionManagerActor.java index 51c158e89d..21fb0708cc 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/SessionManagerActor.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/SessionManagerActor.java @@ -1,15 +1,17 @@ -package im.actor.core.modules.encryption; +package im.actor.core.modules.encryption.ratchet; import java.io.IOException; import java.util.ArrayList; +import java.util.HashSet; import im.actor.core.entity.encryption.PeerSession; import im.actor.core.entity.encryption.PeerSessionsStorage; import im.actor.core.modules.ModuleContext; -import im.actor.core.modules.encryption.entity.PrivateKey; -import im.actor.core.modules.encryption.entity.PublicKey; -import im.actor.core.modules.encryption.entity.UserKeys; -import im.actor.core.modules.encryption.entity.UserKeysGroup; +import im.actor.core.modules.encryption.ratchet.entity.OwnIdentity; +import im.actor.core.modules.encryption.ratchet.entity.PrivateKey; +import im.actor.core.modules.encryption.ratchet.entity.PublicKey; +import im.actor.core.modules.encryption.ratchet.entity.UserKeys; +import im.actor.core.modules.encryption.ratchet.entity.UserKeysGroup; import im.actor.core.util.BaseKeyValueEngine; import im.actor.core.modules.ModuleActor; import im.actor.core.util.RandomUtils; @@ -19,9 +21,6 @@ import im.actor.runtime.crypto.ratchet.RatchetMasterSecret; import im.actor.runtime.crypto.ratchet.RatchetPrivateKey; import im.actor.runtime.crypto.ratchet.RatchetPublicKey; -import im.actor.runtime.function.Function; -import im.actor.runtime.function.FunctionTupled4; -import im.actor.runtime.function.Supplier; import im.actor.runtime.promise.Promise; import im.actor.runtime.promise.Promises; import im.actor.runtime.storage.KeyValueEngine; @@ -35,7 +34,8 @@ public class SessionManagerActor extends ModuleActor { private static final String TAG = "SessionManagerActor"; private KeyValueEngine peerSessions; - private KeyManagerInt keyManager; + private KeyManager keyManager; + private final HashSet locked = new HashSet<>(); public SessionManagerActor(ModuleContext context) { super(context); @@ -44,7 +44,7 @@ public SessionManagerActor(ModuleContext context) { @Override public void preStart() { super.preStart(); - keyManager = context().getEncryption().getKeyManagerInt(); + keyManager = context().getEncryption().getKeyManager(); peerSessions = new BaseKeyValueEngine(Storage.createKeyValue("encryption_sessions")) { @Override @@ -73,46 +73,44 @@ protected PeerSessionsStorage deserialize(byte[] data) { public Promise pickSession(final int uid, final int keyGroupId) { -// return pickCachedSession(uid, keyGroupId) -// .fallback(new Function>() { -// @Override -// public Promise apply(Exception e) { -// return Promises.tuple( -// keyManager.getOwnIdentity(), -// keyManager.getOwnRandomPreKey(), -// keyManager.getUserKeyGroups(uid), -// keyManager.getUserRandomPreKey(uid, keyGroupId)) -// .flatMap(new FunctionTupled4>() { -// @Override -// public Promise apply(KeyManagerActor.OwnIdentity ownIdentity, -// PrivateKey ownPreKey, UserKeys userKeys, -// PublicKey theirPreKey) { -// -// UserKeysGroup keysGroup = ManagedList.of(userKeys.getUserKeysGroups()) -// .filter(UserKeysGroup.BY_KEY_GROUP(keyGroupId)) -// .first(); -// -// spawnSession(uid, -// ownIdentity.getKeyGroup(), -// keyGroupId, -// ownIdentity.getIdentityKey(), -// keysGroup.getIdentityKey(), -// ownPreKey, -// theirPreKey); -// -// return Promise.success(null); -// } -// }); -// } -// }) -// .afterVoid(new Supplier>() { -// @Override -// public Promise get() { -// return pickCachedSession(uid, keyGroupId); -// } -// }); - return null; + PeerSession cached = pickCachedSession(uid, keyGroupId); + if (cached != null) { + return Promise.success(cached); + } + + if (locked.contains(uid)) { + stash(); + return null; + } + locked.add(uid); + + return Promises.tuple( + keyManager.getOwnIdentity(), + keyManager.getOwnRandomPreKey(), + keyManager.getUserKeyGroups(uid), + keyManager.getUserRandomPreKey(uid, keyGroupId)) + .map(tuple -> { + + OwnIdentity ownIdentity = tuple.getT1(); + PrivateKey ownPreKey = tuple.getT2(); + UserKeys userKeys = tuple.getT3(); + PublicKey theirPreKey = tuple.getT4(); + + UserKeysGroup keysGroup = ManagedList.of(userKeys.getUserKeysGroups()) + .filter(UserKeysGroup.BY_KEY_GROUP(keyGroupId)) + .first(); + + return spawnSession(uid, + ownIdentity.getKeyGroup(), + keyGroupId, + ownIdentity.getIdentityKey(), + keysGroup.getIdentityKey(), + ownPreKey, + theirPreKey); + }).after((r, e) -> { + locked.remove(uid); + unstashAll(); + }); } /** @@ -128,33 +126,42 @@ public Promise pickSession(final int uid, final long ownKeyId, final long theirKeyId) { - return pickCachedSession(uid, keyGroupId, ownKeyId, theirKeyId) - .fallback(new Function>() { - @Override - public Promise apply(Exception e) { - return Promises.tuple( - keyManager.getOwnIdentity(), - keyManager.getOwnPreKey(ownKeyId), - keyManager.getUserKeyGroups(uid), - keyManager.getUserPreKey(uid, keyGroupId, theirKeyId)) - .map(new FunctionTupled4() { - @Override - public PeerSession apply(KeyManagerActor.OwnIdentity ownIdentity, PrivateKey ownPreKey, UserKeys userKeys, PublicKey theirPreKey) { - - UserKeysGroup keysGroup = ManagedList.of(userKeys.getUserKeysGroups()) - .filter(UserKeysGroup.BY_KEY_GROUP(keyGroupId)) - .first(); - - return spawnSession(uid, - ownIdentity.getKeyGroup(), - keyGroupId, - ownIdentity.getIdentityKey(), - keysGroup.getIdentityKey(), - ownPreKey, - theirPreKey); - } - }); - } + PeerSession cached = pickCachedSession(uid, keyGroupId, ownKeyId, theirKeyId); + if (cached != null) { + return Promise.success(cached); + } + + if (locked.contains(uid)) { + stash(); + return null; + } + locked.add(uid); + + return Promises.tuple( + keyManager.getOwnIdentity(), + keyManager.getOwnPreKey(ownKeyId), + keyManager.getUserKeyGroups(uid), + keyManager.getUserPreKey(uid, keyGroupId, theirKeyId)) + .map(tuple -> { + OwnIdentity ownIdentity = tuple.getT1(); + PrivateKey ownPreKey = tuple.getT2(); + UserKeys userKeys = tuple.getT3(); + PublicKey theirPreKey = tuple.getT4(); + + UserKeysGroup keysGroup = ManagedList.of(userKeys.getUserKeysGroups()) + .filter(UserKeysGroup.BY_KEY_GROUP(keyGroupId)) + .first(); + + return spawnSession(uid, + ownIdentity.getKeyGroup(), + keyGroupId, + ownIdentity.getIdentityKey(), + keysGroup.getIdentityKey(), + ownPreKey, + theirPreKey); + }).after((r, e) -> { + locked.remove(uid); + unstashAll(); }); } @@ -208,7 +215,7 @@ private PeerSession spawnSession(int uid, PeerSessionsStorage sessionsStorage = peerSessions.getValue(uid); if (sessionsStorage == null) { - sessionsStorage = new PeerSessionsStorage(uid, new ArrayList()); + sessionsStorage = new PeerSessionsStorage(uid, new ArrayList<>()); } sessionsStorage = sessionsStorage.addSession(peerSession); peerSessions.addOrUpdateItem(sessionsStorage); @@ -222,12 +229,12 @@ private PeerSession spawnSession(int uid, * @param keyGroupId Key Group Id * @return promise of session */ - private Promise pickCachedSession(int uid, final int keyGroupId) { + private PeerSession pickCachedSession(int uid, final int keyGroupId) { return ManagedList.of(peerSessions.getValue(uid)) .flatMap(PeerSessionsStorage.SESSIONS) .filter(PeerSession.BY_THEIR_GROUP(keyGroupId)) .sorted(PeerSession.COMPARATOR) - .firstPromise(); + .firstOrNull(); } /** @@ -239,12 +246,12 @@ private Promise pickCachedSession(int uid, final int keyGroupId) { * @param theirKeyId Their Pre key id * @return promise of session */ - private Promise pickCachedSession(int uid, final int keyGroupId, final long ownKeyId, final long theirKeyId) { + private PeerSession pickCachedSession(int uid, final int keyGroupId, final long ownKeyId, final long theirKeyId) { return ManagedList.of(peerSessions.getValue(uid)) .flatMap(PeerSessionsStorage.SESSIONS) .filter(PeerSession.BY_IDS(keyGroupId, ownKeyId, theirKeyId)) .sorted(PeerSession.COMPARATOR) - .firstPromise(); + .firstOrNull(); } // diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/entity/EncryptedMessage.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/entity/EncryptedMessage.java new file mode 100644 index 0000000000..38c8e80940 --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/entity/EncryptedMessage.java @@ -0,0 +1,25 @@ +package im.actor.core.modules.encryption.ratchet.entity; + +import java.util.List; + +import im.actor.core.api.ApiEncryptedBox; +import im.actor.core.api.ApiKeyGroupId; + +public class EncryptedMessage { + + private ApiEncryptedBox encryptedBox; + private List ignoredGroups; + + public EncryptedMessage(ApiEncryptedBox encryptedBox, List ignoredGroups) { + this.encryptedBox = encryptedBox; + this.ignoredGroups = ignoredGroups; + } + + public ApiEncryptedBox getEncryptedBox() { + return encryptedBox; + } + + public List getIgnoredGroups() { + return ignoredGroups; + } +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/entity/EncryptedUserKeys.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/entity/EncryptedUserKeys.java new file mode 100644 index 0000000000..663f28279c --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/entity/EncryptedUserKeys.java @@ -0,0 +1,31 @@ +package im.actor.core.modules.encryption.ratchet.entity; + +import java.util.HashSet; +import java.util.List; + +import im.actor.core.api.ApiEncyptedBoxKey; + +public class EncryptedUserKeys { + + private int uid; + private List boxKeys; + private HashSet ignoredKeys; + + public EncryptedUserKeys(int uid, List boxKeys, HashSet ignoredKeys) { + this.uid = uid; + this.boxKeys = boxKeys; + this.ignoredKeys = ignoredKeys; + } + + public int getUid() { + return uid; + } + + public List getBoxKeys() { + return boxKeys; + } + + public HashSet getIgnoredKeys() { + return ignoredKeys; + } +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/entity/OwnIdentity.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/entity/OwnIdentity.java new file mode 100644 index 0000000000..29aa84b1bc --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/entity/OwnIdentity.java @@ -0,0 +1,22 @@ +package im.actor.core.modules.encryption.ratchet.entity; + +import im.actor.runtime.actors.ask.AskResult; + +public class OwnIdentity extends AskResult { + + private int keyGroup; + private PrivateKey identityKey; + + public OwnIdentity(int keyGroup, PrivateKey identityKey) { + this.keyGroup = keyGroup; + this.identityKey = identityKey; + } + + public int getKeyGroup() { + return keyGroup; + } + + public PrivateKey getIdentityKey() { + return identityKey; + } +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/entity/PrivateKey.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/entity/PrivateKey.java similarity index 98% rename from actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/entity/PrivateKey.java rename to actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/entity/PrivateKey.java index 07d7be2e28..e672c7cd8b 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/entity/PrivateKey.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/entity/PrivateKey.java @@ -1,4 +1,4 @@ -package im.actor.core.modules.encryption.entity; +package im.actor.core.modules.encryption.ratchet.entity; import java.io.IOException; diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/entity/PrivateKeyStorage.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/entity/PrivateKeyStorage.java similarity index 98% rename from actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/entity/PrivateKeyStorage.java rename to actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/entity/PrivateKeyStorage.java index 74a0c65541..b011c4c4fc 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/entity/PrivateKeyStorage.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/entity/PrivateKeyStorage.java @@ -1,4 +1,4 @@ -package im.actor.core.modules.encryption.entity; +package im.actor.core.modules.encryption.ratchet.entity; import java.io.IOException; import java.util.ArrayList; diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/entity/PublicKey.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/entity/PublicKey.java similarity index 95% rename from actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/entity/PublicKey.java rename to actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/entity/PublicKey.java index a246c75033..c08ebcc625 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/entity/PublicKey.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/entity/PublicKey.java @@ -1,4 +1,4 @@ -package im.actor.core.modules.encryption.entity; +package im.actor.core.modules.encryption.ratchet.entity; import java.io.IOException; diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/entity/SessionState.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/entity/SessionState.java new file mode 100644 index 0000000000..8260dce831 --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/entity/SessionState.java @@ -0,0 +1,92 @@ +package im.actor.core.modules.encryption.ratchet.entity; + +import java.io.IOException; + +import im.actor.runtime.Crypto; +import im.actor.runtime.bser.Bser; +import im.actor.runtime.bser.BserObject; +import im.actor.runtime.bser.BserValues; +import im.actor.runtime.bser.BserWriter; +import im.actor.runtime.crypto.Curve25519; + +public class SessionState extends BserObject { + + public static SessionState fromBytes(byte[] data) throws IOException { + return Bser.parse(new SessionState(), data); + } + + private byte[] prevOwnPrivateKey; + private byte[] prevOwnPublicKey; + private byte[] latestOwnPrivateKey; + private byte[] latestOwnPublicKey; + private byte[] latestTheirKey; + + public SessionState(byte[] prevOwnPrivateKey, byte[] prevOwnPublicKey, + byte[] latestOwnPrivateKey, byte[] latestOwnPublicKey, + byte[] latestTheirKey) { + this.prevOwnPrivateKey = prevOwnPrivateKey; + this.prevOwnPublicKey = prevOwnPublicKey; + this.latestOwnPrivateKey = latestOwnPrivateKey; + this.latestOwnPublicKey = latestOwnPublicKey; + this.latestTheirKey = latestTheirKey; + } + + public SessionState() { + + } + + public byte[] getPrevOwnPrivateKey() { + return prevOwnPrivateKey; + } + + public byte[] getPrevOwnPublicKey() { + return prevOwnPublicKey; + } + + public byte[] getLatestOwnPrivateKey() { + return latestOwnPrivateKey; + } + + public byte[] getLatestOwnPublicKey() { + return latestOwnPublicKey; + } + + public byte[] getLatestTheirKey() { + return latestTheirKey; + } + + public SessionState updateKeys(byte[] theirKey) { + byte[] nPrivate = Curve25519.keyGenPrivate(Crypto.randomBytes(32)); + byte[] nPublic = Curve25519.keyGenPublic(nPrivate); + return new SessionState(latestOwnPrivateKey, latestOwnPublicKey, nPrivate, nPublic, + theirKey); + } + + @Override + public void parse(BserValues values) throws IOException { + prevOwnPrivateKey = values.optBytes(1); + prevOwnPublicKey = values.optBytes(2); + latestOwnPrivateKey = values.optBytes(3); + latestOwnPublicKey = values.optBytes(4); + latestTheirKey = values.optBytes(5); + } + + @Override + public void serialize(BserWriter writer) throws IOException { + if (prevOwnPrivateKey != null) { + writer.writeBytes(1, prevOwnPrivateKey); + } + if (prevOwnPublicKey != null) { + writer.writeBytes(2, prevOwnPublicKey); + } + if (latestOwnPrivateKey != null) { + writer.writeBytes(3, latestOwnPrivateKey); + } + if (latestOwnPublicKey != null) { + writer.writeBytes(4, latestOwnPublicKey); + } + if (latestTheirKey != null) { + writer.writeBytes(5, latestTheirKey); + } + } +} \ No newline at end of file diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/entity/UserKeys.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/entity/UserKeys.java similarity index 97% rename from actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/entity/UserKeys.java rename to actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/entity/UserKeys.java index 54af77bfbf..4d789dadea 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/entity/UserKeys.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/entity/UserKeys.java @@ -1,4 +1,4 @@ -package im.actor.core.modules.encryption.entity; +package im.actor.core.modules.encryption.ratchet.entity; import java.io.IOException; import java.util.ArrayList; diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/entity/UserKeysGroup.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/entity/UserKeysGroup.java similarity index 90% rename from actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/entity/UserKeysGroup.java rename to actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/entity/UserKeysGroup.java index 43df64a59e..afcdc34c87 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/entity/UserKeysGroup.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/ratchet/entity/UserKeysGroup.java @@ -1,4 +1,4 @@ -package im.actor.core.modules.encryption.entity; +package im.actor.core.modules.encryption.ratchet.entity; import java.io.IOException; import java.util.ArrayList; @@ -12,12 +12,7 @@ public class UserKeysGroup extends BserObject { public static Predicate BY_KEY_GROUP(final int keyGroupId) { - return new Predicate() { - @Override - public boolean apply(UserKeysGroup keysGroup) { - return keysGroup.getKeyGroupId() == keyGroupId; - } - }; + return keysGroup -> keysGroup.getKeyGroupId() == keyGroupId; } private int keyGroupId; diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/session/EncryptedSession.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/session/EncryptedSession.java deleted file mode 100644 index 9b72a29b07..0000000000 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/session/EncryptedSession.java +++ /dev/null @@ -1,53 +0,0 @@ -package im.actor.core.modules.encryption.session; - -import im.actor.core.modules.encryption.entity.PrivateKey; -import im.actor.core.modules.encryption.entity.PublicKey; -import im.actor.runtime.crypto.ratchet.RatchetMasterSecret; -import im.actor.runtime.crypto.ratchet.RatchetPrivateKey; -import im.actor.runtime.crypto.ratchet.RatchetPublicKey; - -public class EncryptedSession { - private PrivateKey ownIdentityKey; - private PrivateKey ownPreKey; - private PublicKey theirIdentityKey; - private PublicKey theirPreKey; - private int peerKeyGroupId; - private byte[] masterKey; - - public EncryptedSession(PrivateKey ownIdentityKey, PrivateKey ownPreKey, PublicKey theirIdentityKey, PublicKey theirPreKey, int peerKeyGroupId) { - this.ownIdentityKey = ownIdentityKey; - this.ownPreKey = ownPreKey; - this.theirIdentityKey = theirIdentityKey; - this.theirPreKey = theirPreKey; - this.peerKeyGroupId = peerKeyGroupId; - this.masterKey = RatchetMasterSecret.calculateMasterSecret( - new RatchetPrivateKey(ownIdentityKey.getKey()), - new RatchetPrivateKey(ownPreKey.getKey()), - new RatchetPublicKey(theirIdentityKey.getPublicKey()), - new RatchetPublicKey(theirPreKey.getPublicKey())); - } - - public PrivateKey getOwnIdentityKey() { - return ownIdentityKey; - } - - public PrivateKey getOwnPreKey() { - return ownPreKey; - } - - public PublicKey getTheirIdentityKey() { - return theirIdentityKey; - } - - public PublicKey getTheirPreKey() { - return theirPreKey; - } - - public int getPeerKeyGroupId() { - return peerKeyGroupId; - } - - public byte[] getMasterKey() { - return masterKey; - } -} \ No newline at end of file diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/session/EncryptedSessionStorage.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/session/EncryptedSessionStorage.java deleted file mode 100644 index 0d805dd19e..0000000000 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/session/EncryptedSessionStorage.java +++ /dev/null @@ -1,5 +0,0 @@ -package im.actor.core.modules.encryption.session; - -public class EncryptedSessionStorage { - -} \ No newline at end of file diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/updates/EncryptedSequenceProcessor.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/updates/EncryptedSequenceProcessor.java new file mode 100644 index 0000000000..97d73f8ec5 --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/updates/EncryptedSequenceProcessor.java @@ -0,0 +1,9 @@ +package im.actor.core.modules.encryption.updates; + +import im.actor.core.api.ApiEncryptedContent; +import im.actor.runtime.actors.messages.Void; +import im.actor.runtime.promise.Promise; + +public interface EncryptedSequenceProcessor { + Promise onUpdate(int senderId, long date, ApiEncryptedContent update); +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/updates/EncryptedUpdates.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/updates/EncryptedUpdates.java new file mode 100644 index 0000000000..3485cf3929 --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/updates/EncryptedUpdates.java @@ -0,0 +1,38 @@ +package im.actor.core.modules.encryption.updates; + +import im.actor.core.api.ApiEncryptedContent; +import im.actor.core.modules.AbsModule; +import im.actor.core.modules.ModuleContext; +import im.actor.core.modules.messaging.MessagesProcessorEncrypted; +import im.actor.runtime.Log; +import im.actor.runtime.actors.messages.Void; +import im.actor.runtime.promise.Promise; + +public class EncryptedUpdates extends AbsModule { + + private EncryptedSequenceProcessor[] processors; + + public EncryptedUpdates(ModuleContext context) { + super(context); + + processors = new EncryptedSequenceProcessor[]{ + new MessagesProcessorEncrypted(context) + }; + } + + public Promise onUpdate(int senderId, long date, ApiEncryptedContent update) { + Log.d("EncryptedUpdates", "Handling update (from #" + senderId + "): " + update); + + Promise res = null; + for (EncryptedSequenceProcessor s : processors) { + res = s.onUpdate(senderId, date, update); + if (res != null) { + break; + } + } + if (res == null) { + res = Promise.success(null); + } + return res; + } +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/file/DownloadManager.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/file/DownloadManager.java index 56f0c8a54d..9fc6dec563 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/file/DownloadManager.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/file/DownloadManager.java @@ -58,6 +58,7 @@ public void requestState(long fileId, final FileCallback callback) { FileSystemReference reference = Storage.fileFromDescriptor(downloaded1.getDescriptor()); boolean isExist = reference.isExist(); int fileSize = reference.getSize(); + if (isExist && fileSize == downloaded1.getFileSize()) { if (LOG) { Log.d(TAG, "- Downloaded"); @@ -379,8 +380,12 @@ public void onDownloaded(final long fileId, final FileSystemReference reference) return; } - downloaded.addOrUpdateItem(new Downloaded(queueItem.fileReference.getFileId(), - queueItem.fileReference.getFileSize(), reference.getDescriptor())); + int fileSize = queueItem.fileReference.getFileSize(); + if (queueItem.fileReference.getEncryptionInfo() != null) { + fileSize = queueItem.fileReference.getEncryptionInfo().getRealFileSize(); + } + downloaded.addOrUpdateItem(new Downloaded(queueItem.fileReference.getFileId(), fileSize, + reference.getDescriptor())); queue.remove(queueItem); queueItem.taskRef.send(PoisonPill.INSTANCE); diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/file/DownloadTask.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/file/DownloadTask.java index 93200d7de6..c3aef7f0be 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/file/DownloadTask.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/file/DownloadTask.java @@ -7,17 +7,31 @@ import im.actor.core.entity.FileReference; import im.actor.core.modules.ModuleContext; import im.actor.core.modules.ModuleActor; +import im.actor.runtime.Crypto; import im.actor.runtime.HTTP; import im.actor.runtime.Log; import im.actor.runtime.Storage; import im.actor.runtime.actors.ActorRef; import im.actor.runtime.actors.ActorCancellable; +import im.actor.runtime.actors.messages.Void; +import im.actor.runtime.crypto.BlockCipher; +import im.actor.runtime.crypto.primitives.modes.CBCBlockCipherStream; +import im.actor.runtime.crypto.primitives.util.ByteStrings; import im.actor.runtime.files.FileSystemReference; import im.actor.runtime.files.OutputFile; +import im.actor.runtime.function.Consumer; +import im.actor.runtime.function.Supplier; import im.actor.runtime.http.HTTPError; +import im.actor.runtime.http.HTTPResponse; +import im.actor.runtime.promise.Promise; +import im.actor.runtime.promise.PromiseFunc; +import im.actor.runtime.promise.Promises; public class DownloadTask extends ModuleActor { + // j2objc workaround + private static final Void DUMB = null; + private static final int SIM_BLOCKS_COUNT = 4; private static final int NOTIFY_THROTTLE = 1000; private static final int DEFAULT_RETRY = 15; @@ -26,6 +40,7 @@ public class DownloadTask extends ModuleActor { private final boolean LOG; private FileReference fileReference; + private BlockCipher encryptionCipher; private ActorRef manager; private FileSystemReference destReference; @@ -40,9 +55,6 @@ public class DownloadTask extends ModuleActor { private String fileUrl; private int blockSize = 128 * 1024; private int blocksCount; - private int nextBlock = 0; - private int currentDownloads = 0; - private int downloaded = 0; public DownloadTask(FileReference fileReference, ActorRef manager, ModuleContext context) { super(context); @@ -67,7 +79,11 @@ public void preStart() { return; } - destReference.openWrite(fileReference.getFileSize()).then(r -> { + int destSize = fileReference.getFileSize(); + if (fileReference.getEncryptionInfo() != null) { + destSize = fileReference.getFileSize(); + } + destReference.openWrite(destSize).then(r -> { outputFile = r; requestUrl(); }).failure(e -> { @@ -78,16 +94,6 @@ public void preStart() { }); } - @Override - public void onReceive(Object message) { - if (message instanceof Retry) { - Retry retry = (Retry) message; - retryPart(retry.getBlockIndex(), retry.getFileOffset(), retry.getAttempt()); - } else { - super.onReceive(message); - } - } - private void requestUrl() { if (LOG) { Log.d(TAG, "Loading url..."); @@ -116,7 +122,63 @@ private void startDownload() { if (LOG) { Log.d(TAG, "Starting downloading " + blocksCount + " blocks"); } - checkQueue(); + + Promises.traverseParallel(SIM_BLOCKS_COUNT, partsSupplier(), new Consumer() { + + int index = 0; + + @Override + public void apply(HTTPResponse r) { + reportProgress((index + 1) / (float) blocksCount); + + if (fileReference.getEncryptionInfo() != null) { + + int offset = 0; + int padding = 0; + + if (index == 0) { + byte[] iv = ByteStrings.substring(r.getContent(), 0, 16); + byte[] key = ByteStrings.substring(fileReference.getEncryptionInfo().getKey(), 0, 32); + Log.d(TAG, "File IV: " + Crypto.hex(iv)); + Log.d(TAG, "File Key: " + Crypto.hex(key)); + Log.d(TAG, "File Size: " + fileReference.getFileSize()); + Log.d(TAG, "File Real Size: " + fileReference.getEncryptionInfo().getRealFileSize()); + encryptionCipher = new CBCBlockCipherStream(iv, Crypto.createAES256(key)); + offset = 16; + } + + if (index == blocksCount - 1) { + padding = (fileReference.getFileSize() - 16 - fileReference.getEncryptionInfo().getRealFileSize()); + } + + byte[] dest = new byte[r.getContent().length - offset]; + for (int i = 0; i < (dest.length - offset) / encryptionCipher.getBlockSize(); i++) { + encryptionCipher.decryptBlock(r.getContent(), offset + i * encryptionCipher.getBlockSize(), + dest, i * encryptionCipher.getBlockSize()); + } + + if (index == 0) { + if (!outputFile.write(index * blockSize, dest, 0, dest.length - padding)) { + throw new RuntimeException("Unable to write file"); + } + } else { + if (!outputFile.write(index * blockSize - 16, dest, 0, dest.length - padding)) { + throw new RuntimeException("Unable to write file"); + } + } + } else { + if (!outputFile.write(index * blockSize, r.getContent(), 0, r.getContent().length)) { + throw new RuntimeException("Unable to write file"); + } + } + + index++; + } + }).then(r -> { + completeDownload(); + }).failure(e -> { + completeWithError(); + }); } private void completeDownload() { @@ -145,82 +207,62 @@ private void completeDownload() { reportComplete(reference); } - private void checkQueue() { - if (isCompleted) { - return; - } - - if (LOG) { - Log.d(TAG, "checkQueue " + currentDownloads + "/" + nextBlock); - } - if (currentDownloads == 0 && nextBlock >= blocksCount) { - completeDownload(); - } else if (currentDownloads < SIM_BLOCKS_COUNT && nextBlock < blocksCount) { - currentDownloads++; - int blockIndex = nextBlock++; - int offset = blockIndex * blockSize; - - if (LOG) { - Log.d(TAG, "Starting part #" + blockIndex + " download"); - } - - downloadPart(blockIndex, offset, 0); - - checkQueue(); - } else { - if (LOG) { - Log.d(TAG, "Task queue is full"); - } - } + private void completeWithError() { + reportError(); } - private void retryPart(int blockIndex, int fileOffset, int attempt) { - if (isCompleted) { - return; - } - - if (LOG) { - Log.d(TAG, "Trying again part #" + blockIndex + " download"); - } + // Downloading parts - downloadPart(blockIndex, fileOffset, attempt); - } + private Supplier> partsSupplier() { + return new Supplier>() { + int nextBlock = 0; - private void downloadPart(final int blockIndex, final int fileOffset, final int attempt) { - HTTP.getMethod(fileUrl, fileOffset, blockSize, fileReference.getFileSize()).then(r -> { - downloaded++; - if (LOG) { - Log.d(TAG, "Download part #" + blockIndex + " completed"); - } - if (!outputFile.write(fileOffset, r.getContent(), 0, r.getContent().length)) { - reportError(); - return; + @Override + public Promise get() { + if (nextBlock >= blocksCount) { + return null; + } + int blockIndex = nextBlock++; + int offset = blockIndex * blockSize; + return downloadPartPromise(blockIndex, offset, 0); } - currentDownloads--; - reportProgress(downloaded / (float) blocksCount); - checkQueue(); - }).failure(e -> { - if ((e instanceof HTTPError) - && ((((HTTPError) e).getErrorCode() >= 500 - && ((HTTPError) e).getErrorCode() < 600) - || ((HTTPError) e).getErrorCode() == 0)) { - // Server on unknown error - int retryInSecs = DEFAULT_RETRY; + }; + } + private Promise downloadPartPromise(int blockIndex, int fileOffset, int attempt) { + return new Promise<>((PromiseFunc) resolver -> { + HTTP.getMethod(fileUrl, fileOffset, blockSize, fileReference.getFileSize()).then(r -> { if (LOG) { - Log.w(TAG, "Download part #" + blockIndex + " failure #" + ((HTTPError) e).getErrorCode() + " trying again in " + retryInSecs + " sec, attempt #" + (attempt + 1)); + Log.d(TAG, "Download part #" + blockIndex + " completed"); } - - self().send(new Retry(blockIndex, fileOffset, attempt + 1)); - } else { - if (LOG) { - Log.d(TAG, "Download part #" + blockIndex + " failure"); + resolver.result(r); + }).failure(e -> { + if ((e instanceof HTTPError) + && ((((HTTPError) e).getErrorCode() >= 500 + && ((HTTPError) e).getErrorCode() < 600) + || ((HTTPError) e).getErrorCode() == 0)) { + // Server on unknown error + int retryInSecs = DEFAULT_RETRY; + + if (LOG) { + Log.w(TAG, "Download part #" + blockIndex + " failure #" + ((HTTPError) e).getErrorCode() + " trying again in " + retryInSecs + " sec, attempt #" + (attempt + 1)); + } + + schedule((Runnable) () -> { + downloadPartPromise(blockIndex, fileOffset, attempt + 1).pipeTo(resolver); + }, retryInSecs * 1000L); + } else { + if (LOG) { + Log.d(TAG, "Download part #" + blockIndex + " failure"); + } + resolver.error(e); } - reportError(); - } + }); }); } + // Reporting + private void reportError() { if (isCompleted) { return; @@ -267,29 +309,4 @@ private void reportComplete(FileSystemReference reference) { isCompleted = true; manager.send(new DownloadManager.OnDownloaded(fileReference.getFileId(), reference)); } - - private class Retry { - - private int blockIndex; - private int fileOffset; - private int attempt; - - public Retry(int blockIndex, int fileOffset, int attempt) { - this.blockIndex = blockIndex; - this.fileOffset = fileOffset; - this.attempt = attempt; - } - - public int getBlockIndex() { - return blockIndex; - } - - public int getFileOffset() { - return fileOffset; - } - - public int getAttempt() { - return attempt; - } - } } diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/file/FilesModule.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/file/FilesModule.java index a13a13b37d..3bf890039e 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/file/FilesModule.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/file/FilesModule.java @@ -10,6 +10,7 @@ import im.actor.core.modules.AbsModule; import im.actor.core.modules.ModuleContext; import im.actor.core.modules.file.entity.Downloaded; +import im.actor.core.modules.file.entity.EncryptionInfo; import im.actor.core.util.BaseKeyValueEngine; import im.actor.core.viewmodel.FileCallback; import im.actor.core.viewmodel.FileEventCallback; @@ -121,8 +122,9 @@ public void unbindUploadFile(long rid, UploadFileCallback callback) { uploadManager.send(new UploadManager.UnbindUpload(rid, callback)); } - public void requestUpload(long rid, String descriptor, String fileName, ActorRef requester) { - uploadManager.send(new UploadManager.StartUpload(rid, descriptor, fileName), requester); + public void requestUpload(long rid, String descriptor, String fileName, boolean encrypt, + ActorRef requester) { + uploadManager.send(new UploadManager.StartUpload(rid, descriptor, fileName, encrypt), requester); } public void cancelUpload(long rid) { diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/file/UploadManager.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/file/UploadManager.java index f0b7e56887..c18fa3418f 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/file/UploadManager.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/file/UploadManager.java @@ -11,6 +11,7 @@ import im.actor.core.modules.ModuleContext; import im.actor.core.modules.file.entity.Downloaded; import im.actor.core.modules.ModuleActor; +import im.actor.core.modules.file.entity.EncryptionInfo; import im.actor.core.util.RandomUtils; import im.actor.core.viewmodel.UploadFileCallback; import im.actor.runtime.Log; @@ -37,11 +38,12 @@ public UploadManager(ModuleContext context) { // Tasks - public void startUpload(long rid, String descriptor, String fileName, ActorRef requestActor) { + public void startUpload(long rid, String descriptor, String fileName, + boolean doEncrypt, ActorRef requestActor) { if (LOG) { Log.d(TAG, "Starting upload #" + rid + " with descriptor " + descriptor); } - QueueItem queueItem = new QueueItem(rid, descriptor, fileName, requestActor); + QueueItem queueItem = new QueueItem(rid, descriptor, fileName, doEncrypt, requestActor); queueItem.isStopped = false; queue.add(queueItem); checkQueue(); @@ -294,7 +296,7 @@ private void checkQueue() { final QueueItem finalPendingQueue = pendingQueue; pendingQueue.taskRef = system().actorOf(Props.create(() -> new UploadTask(finalPendingQueue.rid, finalPendingQueue.fileDescriptor, - finalPendingQueue.fileName, self(), context())).changeDispatcher("heavy"), "actor/upload/task_" + RandomUtils.nextRid()); + finalPendingQueue.fileName, finalPendingQueue.doEncrypt, self(), context())).changeDispatcher("heavy"), "actor/upload/task_" + RandomUtils.nextRid()); } private QueueItem findItem(long rid) { @@ -312,15 +314,17 @@ private class QueueItem { private boolean isStopped; private boolean isStarted; private float progress; + private boolean doEncrypt; private ActorRef taskRef; private ActorRef requestActor; private String fileName; - private QueueItem(long rid, String fileDescriptor, String fileName, ActorRef requestActor) { + private QueueItem(long rid, String fileDescriptor, String fileName, boolean doEncrypt, ActorRef requestActor) { this.rid = rid; this.fileDescriptor = fileDescriptor; this.requestActor = requestActor; this.fileName = fileName; + this.doEncrypt = doEncrypt; } } @@ -331,7 +335,7 @@ public void onReceive(Object message) { if (message instanceof StartUpload) { StartUpload startUpload = (StartUpload) message; startUpload(startUpload.getRid(), startUpload.getFileDescriptor(), - startUpload.getFileName(), sender()); + startUpload.getFileName(), startUpload.isDoEncrypt(), sender()); } else if (message instanceof StopUpload) { StopUpload cancelUpload = (StopUpload) message; stopUpload(cancelUpload.getRid()); @@ -369,11 +373,13 @@ public static class StartUpload { private long rid; private String fileDescriptor; private String fileName; + private boolean doEncrypt; - public StartUpload(long rid, String fileDescriptor, String fileName) { + public StartUpload(long rid, String fileDescriptor, String fileName, boolean doEncrypt) { this.rid = rid; this.fileDescriptor = fileDescriptor; this.fileName = fileName; + this.doEncrypt = doEncrypt; } public long getRid() { @@ -387,6 +393,10 @@ public String getFileDescriptor() { public String getFileName() { return fileName; } + + public boolean isDoEncrypt() { + return doEncrypt; + } } public static class BindUpload { diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/file/UploadTask.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/file/UploadTask.java index eb89b913e5..01479313bb 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/file/UploadTask.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/file/UploadTask.java @@ -4,28 +4,31 @@ package im.actor.core.modules.file; +import im.actor.core.api.ApiDocumentEncryptionInfo; import im.actor.core.api.rpc.RequestCommitFileUpload; import im.actor.core.api.rpc.RequestGetFileUploadPartUrl; import im.actor.core.api.rpc.RequestGetFileUploadUrl; -import im.actor.core.api.rpc.ResponseCommitFileUpload; -import im.actor.core.api.rpc.ResponseGetFileUploadUrl; import im.actor.core.entity.FileReference; import im.actor.core.modules.ModuleContext; import im.actor.core.modules.ModuleActor; -import im.actor.core.network.RpcCallback; -import im.actor.core.network.RpcException; +import im.actor.runtime.Crypto; import im.actor.runtime.HTTP; import im.actor.runtime.Log; import im.actor.runtime.Storage; import im.actor.runtime.actors.ActorRef; import im.actor.runtime.actors.ActorCancellable; +import im.actor.runtime.crypto.BlockCipher; import im.actor.runtime.crypto.CRC32; +import im.actor.runtime.crypto.primitives.modes.CBCBlockCipherStream; +import im.actor.runtime.crypto.primitives.util.ByteStrings; import im.actor.runtime.files.FileSystemReference; -import im.actor.runtime.files.InputFile; import im.actor.runtime.files.OutputFile; +import im.actor.runtime.files.SequenceFileSystemInputFile; +import im.actor.runtime.files.SequenceInputFile; import im.actor.runtime.http.HTTPError; import im.actor.runtime.http.HTTPResponse; import im.actor.runtime.promise.Promise; +import im.actor.runtime.util.Hex; public class UploadTask extends ModuleActor { @@ -35,18 +38,23 @@ public class UploadTask extends ModuleActor { private static final int SIM_BLOCKS_COUNT = 4; private static final int NOTIFY_THROTTLE = 1000; private static final int DEFAULT_RETRY = 15; + private static final int IV_SIZE = 16; private final String TAG; private final boolean LOG; - private long rid; - private String fileName; - private String descriptor; + private final long rid; + private final String fileName; + private final String descriptor; + private final boolean isEncrypted; + private BlockCipher encryptionCipher; + private byte[] encryptionKey; + private byte[] encryptionIv; private boolean isWriteToDestProvider = false; private FileSystemReference srcReference; - private InputFile inputFile; + private SequenceInputFile inputFile; private FileSystemReference destReference; private OutputFile outputFile; @@ -54,6 +62,7 @@ public class UploadTask extends ModuleActor { private ActorRef manager; private boolean isCompleted = false; + private int finalSize; private int blockSize = 128 * 1024; private int blocksCount; private int nextBlock = 0; @@ -68,12 +77,14 @@ public class UploadTask extends ModuleActor { private float currentProgress; private boolean alreadyInTemp; - public UploadTask(long rid, String descriptor, String fileName, ActorRef manager, ModuleContext context) { + public UploadTask(long rid, String descriptor, String fileName, boolean isEncrypted, + ActorRef manager, ModuleContext context) { super(context); this.LOG = context.getConfiguration().isEnableFilesLogging(); this.rid = rid; this.fileName = fileName; this.descriptor = descriptor; + this.isEncrypted = isEncrypted; this.manager = manager; this.TAG = "UploadTask{" + rid + "}"; } @@ -103,45 +114,60 @@ public void preStart() { } } - srcReference.openRead() - .flatMap(f -> { - inputFile = f; - if (isWriteToDestProvider) { - return destReference.openWrite(srcReference.getSize()); - } else { - return Promise.success(null); - } - }) - .flatMap(f -> { - outputFile = f; + srcReference.openRead().flatMap(f -> { + inputFile = new SequenceFileSystemInputFile(f); + if (isWriteToDestProvider) { + return destReference.openWrite(srcReference.getSize()); + } else { + return Promise.success(null); + } + }).flatMap(f -> { + outputFile = f; - crc32 = new CRC32(); + // CRC + crc32 = new CRC32(); - blocksCount = srcReference.getSize() / blockSize; - if (srcReference.getSize() % blockSize != 0) { - blocksCount++; - } + // File Size + finalSize = srcReference.getSize(); - if (LOG) { - Log.d(TAG, "Starting uploading " + blocksCount + " blocks"); - Log.d(TAG, "Requesting upload config..."); - } + // Encryption + if (isEncrypted) { + encryptionKey = Crypto.randomBytes(32); + encryptionIv = Crypto.randomBytes(16); - return api(new RequestGetFileUploadUrl(srcReference.getSize())); - }) - .then(r -> { - if (LOG) { - Log.d(TAG, "Upload config loaded"); - } - uploadConfig = r.getUploadKey(); - checkQueue(); - }) - .failure(e -> { - if (LOG) { - Log.w(TAG, "Error during initialization of upload"); - } - reportError(); - }); + Log.d(TAG, "File IV: " + Crypto.hex(encryptionIv)); + Log.d(TAG, "File Key: " + Crypto.hex(encryptionKey)); + + encryptionCipher = new CBCBlockCipherStream(encryptionIv, Crypto.createAES256(encryptionKey)); + + // Overwrite file size + finalSize = 16/*IV*/ + Crypto.paddedLength(srcReference.getSize(), encryptionCipher.getBlockSize()); + } + + // Blocks + blocksCount = finalSize / blockSize; + if (finalSize % blockSize != 0) { + blocksCount++; + } + + if (LOG) { + Log.d(TAG, "Starting uploading " + blocksCount + " blocks"); + Log.d(TAG, "Requesting upload config..."); + } + + return api(new RequestGetFileUploadUrl(finalSize)); + }).then(r -> { + if (LOG) { + Log.d(TAG, "Upload config loaded"); + } + uploadConfig = r.getUploadKey(); + checkQueue(); + }).failure(e -> { + if (LOG) { + Log.w(TAG, "Error during initialization of upload"); + } + reportError(); + }); } private void checkQueue() { @@ -164,32 +190,34 @@ private void checkQueue() { outputFile.close(); } - request(new RequestCommitFileUpload(uploadConfig, fileName), new RpcCallback() { - @Override - public void onResult(ResponseCommitFileUpload response) { - if (LOG) { - Log.d(TAG, "Upload completed..."); - } - - FileReference location = new FileReference(response.getUploadedFileLocation(), - fileName, srcReference.getSize()); - - if (isWriteToDestProvider || alreadyInTemp) { - FileSystemReference reference = Storage.commitTempFile(alreadyInTemp ? srcReference : destReference, location.getFileId(), - location.getFileName()); - reportComplete(location, reference); - } else { - reportComplete(location, srcReference); - } + api(new RequestCommitFileUpload(uploadConfig, fileName)).then(r -> { + if (LOG) { + Log.d(TAG, "Upload completed..."); } - @Override - public void onError(RpcException e) { - if (LOG) { - Log.w(TAG, "Upload complete error"); - } - reportError(); + ApiDocumentEncryptionInfo encryptionInfo; + if (isEncrypted) { + encryptionInfo = new ApiDocumentEncryptionInfo(srcReference.getSize(), + "aes128-hmac", encryptionKey); + } else { + encryptionInfo = null; + } + + FileReference location = new FileReference(r.getUploadedFileLocation(), fileName, + srcReference.getSize(), encryptionInfo); + + if (isWriteToDestProvider || alreadyInTemp) { + FileSystemReference reference = Storage.commitTempFile(alreadyInTemp ? srcReference : destReference, location.getFileId(), + location.getFileName()); + reportComplete(location, reference); + } else { + reportComplete(location, srcReference); + } + }).failure(e -> { + if (LOG) { + Log.w(TAG, "Upload complete error"); } + reportError(); }); return; } @@ -202,12 +230,27 @@ public void onError(RpcException e) { private void loadPart(final int blockIndex) { int size = blockSize; int fileOffset = blockIndex * blockSize; - if ((blockIndex + 1) * blockSize > srcReference.getSize()) { - size = srcReference.getSize() - blockIndex * blockSize; + + // Calculating appropriate read block size + if (isEncrypted) { + if (blockIndex == 0) { + if (srcReference.getSize() - IV_SIZE > blockSize) { + size = blockSize - IV_SIZE; + } else { + size = srcReference.getSize(); + } + } else { + if ((blockIndex + 1) * blockSize - IV_SIZE > srcReference.getSize()) { + size = srcReference.getSize() - blockIndex * blockSize; + } + } + } else { + if ((blockIndex + 1) * blockSize > srcReference.getSize()) { + size = srcReference.getSize() - blockIndex * blockSize; + } } - // TODO: Validate file part load ordering - inputFile.read(fileOffset, size).then(filePart -> { + inputFile.readBlock(size).then(filePart -> { if (isCompleted) { return; } @@ -232,7 +275,46 @@ private void loadPart(final int blockIndex) { } uploadCount++; - uploadPart(blockIndex, filePart.getContents(), 0); + + // Block Encryption + if (isEncrypted) { + // Result Block + int destBlockCount = (int) Math.ceil(filePart.getContents().length / + (float) encryptionCipher.getBlockSize()); + int destBlockSize = destBlockCount * encryptionCipher.getBlockSize(); + int destBlockOffset = 0; + if (blockIndex == 0) { + destBlockOffset = 16; + } + + // Block for uploading + byte[] res = new byte[destBlockSize + destBlockOffset]; + + // Appending IV if needed + if (blockIndex == 0) { + for (int i = 0; i < IV_SIZE; i++) { + res[i] = encryptionIv[i]; + } + } + + // Encrypting Block + for (int i = 0; i < destBlockCount; i++) { + if (i == destBlockCount - 1) { + byte[] tmp = new byte[encryptionCipher.getBlockSize()]; + for (int j = 0; j < encryptionCipher.getBlockSize() && i * encryptionCipher.getBlockSize() + j < filePart.getContents().length; j++) { + tmp[j] = filePart.getContents()[j]; + } + encryptionCipher.encryptBlock(tmp, 0, res, destBlockOffset + i * encryptionCipher.getBlockSize()); + } else { + encryptionCipher.encryptBlock(filePart.getContents(), i * encryptionCipher.getBlockSize(), + res, destBlockOffset + i * encryptionCipher.getBlockSize()); + } + } + + uploadPart(blockIndex, res, 0); + } else { + uploadPart(blockIndex, filePart.getContents(), 0); + } checkQueue(); }).failure(e -> { if (isCompleted) { diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/file/entity/EncryptionInfo.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/file/entity/EncryptionInfo.java new file mode 100644 index 0000000000..8c16de6a15 --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/file/entity/EncryptionInfo.java @@ -0,0 +1,47 @@ +package im.actor.core.modules.file.entity; + +import java.io.IOException; + +import im.actor.runtime.bser.Bser; +import im.actor.runtime.bser.BserObject; +import im.actor.runtime.bser.BserValues; +import im.actor.runtime.bser.BserWriter; + +public class EncryptionInfo extends BserObject { + + public static EncryptionInfo fromBytes(byte[] data) throws IOException { + return Bser.parse(new EncryptionInfo(), data); + } + + private byte[] encryptionKey; + private byte[] macKey; + + public EncryptionInfo(byte[] encryptionKey, byte[] macKey) { + this.encryptionKey = encryptionKey; + this.macKey = macKey; + } + + private EncryptionInfo() { + + } + + public byte[] getEncryptionKey() { + return encryptionKey; + } + + public byte[] getMacKey() { + return macKey; + } + + @Override + public void parse(BserValues values) throws IOException { + encryptionKey = values.getBytes(1); + macKey = values.getBytes(2); + } + + @Override + public void serialize(BserWriter writer) throws IOException { + writer.writeBytes(1, encryptionKey); + writer.writeBytes(2, macKey); + } +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/MessagesModule.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/MessagesModule.java index 7cc463e32b..7caf96729a 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/MessagesModule.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/MessagesModule.java @@ -10,6 +10,8 @@ import java.util.ArrayList; import java.util.HashMap; +import im.actor.core.api.ApiEncryptedDeleteAll; +import im.actor.core.api.ApiEncryptedEditContent; import im.actor.core.api.ApiMessage; import im.actor.core.api.ApiOutPeer; import im.actor.core.api.ApiPeer; @@ -27,6 +29,7 @@ import im.actor.core.api.rpc.ResponseDialogsOrder; import im.actor.core.api.rpc.ResponseLoadArchived; import im.actor.core.api.rpc.ResponseReactionsResponse; +import im.actor.core.api.rpc.ResponseSendEncryptedPackage; import im.actor.core.api.rpc.ResponseSeq; import im.actor.core.api.rpc.ResponseSeqDate; import im.actor.core.api.updates.UpdateChatClear; @@ -51,6 +54,7 @@ import im.actor.core.events.PeerChatPreload; import im.actor.core.modules.AbsModule; import im.actor.core.modules.ModuleContext; +import im.actor.core.modules.messaging.actions.DestructorActor; import im.actor.core.modules.messaging.dialogs.DialogsInt; import im.actor.core.modules.messaging.history.ArchivedDialogsActor; import im.actor.core.modules.messaging.actions.CursorReaderActor; @@ -65,6 +69,7 @@ import im.actor.core.network.RpcCallback; import im.actor.core.network.RpcException; import im.actor.core.network.RpcInternalException; +import im.actor.core.util.RandomUtils; import im.actor.core.viewmodel.Command; import im.actor.core.viewmodel.CommandCallback; import im.actor.core.viewmodel.ConversationVM; @@ -99,13 +104,14 @@ public class MessagesModule extends AbsModule implements BusSubscriber { private ListEngine

dialogs; private DialogsInt dialogsInt; + private RouterInt router; + private ActorRef dialogsHistoryActor; private ActorRef archivedDialogsActor; private ActorRef plainReadActor; private ActorRef plainReceiverActor; private ActorRef sendMessageActor; private ActorRef deletionsActor; - private RouterInt router; private final HashMap historyLoaderActors = new HashMap<>(); private MVVMCollection conversationStates; @@ -129,9 +135,10 @@ public MessagesModule(final ModuleContext context) { public void run() { + this.dialogsInt = new DialogsInt(context()); + this.router = new RouterInt(context()); - this.dialogsInt = new DialogsInt(context()); this.dialogsHistoryActor = system().actorOf("actor/dialogs/history", () -> new DialogsHistoryActor(context())); this.archivedDialogsActor = system().actorOf("actor/dialogs/archived", () -> new ArchivedDialogsActor(context())); @@ -281,47 +288,51 @@ public void sendDocument(Peer peer, String fileName, String mimeType, FastThumb public Promise updateMessage(final Peer peer, final String message, final long rid) { context().getTypingModule().onMessageSent(peer); - ArrayList mentions = new ArrayList<>(); - TextContent content = TextContent.create(message, null, mentions); - if (peer.getPeerType() == PeerType.GROUP) { - Group group = groups().getValue(peer.getPeerId()); - String lowText = message.toLowerCase(); - for (GroupMember member : group.getMembers()) { - User user = users().getValue(member.getUid()); - if (user.getNick() != null) { - String nick = "@" + user.getNick().toLowerCase(); - // TODO: Better filtering - if (lowText.contains(nick + ":") - || lowText.contains(nick + " ") - || lowText.contains(" " + nick) - || lowText.endsWith(nick) - || lowText.equals(nick)) { - mentions.add(user.getUid()); + + if (peer.getPeerType() == PeerType.PRIVATE_ENCRYPTED) { + ApiEncryptedEditContent editContent = new ApiEncryptedEditContent( + peer.getPeerId(), rid, new ApiTextMessage(message, new ArrayList<>(), null)); + return context().getEncryption().doSend(RandomUtils.nextRid(), editContent, peer.getPeerId()).then(r -> { + context().getEncryption().onUpdate(myUid(), r, editContent); + }).flatMap(r -> null); + } else { + ArrayList mentions = new ArrayList<>(); + TextContent content = TextContent.create(message, null, mentions); + if (peer.getPeerType() == PeerType.GROUP) { + Group group = groups().getValue(peer.getPeerId()); + String lowText = message.toLowerCase(); + for (GroupMember member : group.getMembers()) { + User user = users().getValue(member.getUid()); + if (user.getNick() != null) { + String nick = "@" + user.getNick().toLowerCase(); + // TODO: Better filtering + if (lowText.contains(nick + ":") + || lowText.contains(nick + " ") + || lowText.contains(" " + nick) + || lowText.endsWith(nick) + || lowText.equals(nick)) { + mentions.add(user.getUid()); + } } } } + ApiMessage editMessage = new ApiTextMessage(message, content.getMentions(), content.getTextMessageEx()); + + return buildOutPeer(peer) + .flatMap(apiOutPeer -> + api(new RequestUpdateMessage(apiOutPeer, rid, editMessage))) + .flatMap(responseSeqDate -> + updates().applyUpdate( + responseSeqDate.getSeq(), + responseSeqDate.getState(), + new UpdateMessageContentChanged( + new ApiPeer(peer.getPeerType().toApi(), peer.getPeerId()), + rid, + editMessage) + )); } - ApiMessage editMessage = new ApiTextMessage(message, content.getMentions(), content.getTextMessageEx()); - - return buildOutPeer(peer) - .flatMap(apiOutPeer -> - api(new RequestUpdateMessage(apiOutPeer, rid, editMessage))) - .flatMap(responseSeqDate -> - updates().applyUpdate( - responseSeqDate.getSeq(), - responseSeqDate.getState(), - new UpdateMessageContentChanged( - new ApiPeer(peer.getPeerType().toApi(), peer.getPeerId()), - rid, - editMessage) - )); } - public Promise chatIsEmpty(Peer peer) { - return new Promise<>(resolver -> resolver.result(getConversationEngine(peer).getCount() == 0)); - } - - public void forwardContent(Peer peer, AbsContent content) { sendMessageActor.send(new SenderActor.ForwardContent(peer, content)); } @@ -331,22 +342,30 @@ public void sendSticker(@NotNull Peer peer, sendMessageActor.send(new SenderActor.SendSticker(peer, sticker)); } - public void saveDraft(Peer peer, String draft) { - context().getSettingsModule().setStringValue("drafts_" + peer.getUnuqueId(), draft); + if (peer.getPeerType() == PeerType.PRIVATE || peer.getPeerType() == PeerType.GROUP) { + context().getSettingsModule().setStringValue("drafts_" + peer.getUnuqueId(), draft); + } } public String loadDraft(Peer peer) { - String res = context().getSettingsModule().getStringValue("drafts_" + peer.getUnuqueId(), null); - if (res == null) { - return ""; + if (peer.getPeerType() == PeerType.PRIVATE || peer.getPeerType() == PeerType.GROUP) { + String res = context().getSettingsModule().getStringValue("drafts_" + peer.getUnuqueId(), null); + if (res == null) { + return ""; + } else { + return res; + } } else { - return res; + return ""; } } public Promise addReaction(final Peer peer, final long rid, final String reaction) { + if (peer.getPeerType() == PeerType.PRIVATE_ENCRYPTED) { + return Promise.failure(new RuntimeException("Unsupported in secret chats")); + } return buildOutPeer(peer) .flatMap(apiOutPeer -> api(new RequestMessageSetReaction(apiOutPeer, rid, reaction))) @@ -360,6 +379,9 @@ public Promise addReaction(final Peer peer, final long rid, final String r } public Promise removeReaction(final Peer peer, final long rid, final String reaction) { + if (peer.getPeerType() == PeerType.PRIVATE_ENCRYPTED) { + return Promise.failure(new RuntimeException("Unsupported in secret chats")); + } return buildOutPeer(peer) .flatMap(apiOutPeer -> api(new RequestMessageRemoveReaction(apiOutPeer, rid, reaction))) @@ -405,27 +427,37 @@ public Promise archiveChat(final Peer peer) { public Promise deleteChat(final Peer peer) { - return buildOutPeer(peer) - .flatMap(apiOutPeer -> - api(new RequestDeleteChat(apiOutPeer))) - .flatMap(responseSeq -> - updates().applyUpdate( - responseSeq.getSeq(), - responseSeq.getState(), - new UpdateChatDelete(new ApiPeer(peer.getPeerType().toApi(), peer.getPeerId())) - )); + if (peer.getPeerType() == PeerType.PRIVATE_ENCRYPTED) { + return context().getEncryption().doSend(new ApiEncryptedDeleteAll(peer.getPeerId()), peer.getPeerId()) + .flatMap(v -> getRouter().onSecretChatDeleted(peer)); + } else { + return buildOutPeer(peer) + .flatMap(apiOutPeer -> + api(new RequestDeleteChat(apiOutPeer))) + .flatMap(responseSeq -> + updates().applyUpdate( + responseSeq.getSeq(), + responseSeq.getState(), + new UpdateChatDelete(new ApiPeer(peer.getPeerType().toApi(), peer.getPeerId())) + )); + } } public Promise clearChat(final Peer peer) { - return buildOutPeer(peer) - .flatMap(apiOutPeer -> - api(new RequestClearChat(apiOutPeer))) - .flatMap(responseSeq -> - updates().applyUpdate( - responseSeq.getSeq(), - responseSeq.getState(), - new UpdateChatClear(new ApiPeer(peer.getPeerType().toApi(), peer.getPeerId()))) - ); + if (peer.getPeerType() == PeerType.PRIVATE_ENCRYPTED) { + return context().getEncryption().doSend(new ApiEncryptedDeleteAll(peer.getPeerId()), peer.getPeerId()) + .flatMap(v -> getRouter().onSecretChatCleared(peer)); + } else { + return buildOutPeer(peer) + .flatMap(apiOutPeer -> + api(new RequestClearChat(apiOutPeer))) + .flatMap(responseSeq -> + updates().applyUpdate( + responseSeq.getSeq(), + responseSeq.getState(), + new UpdateChatClear(new ApiPeer(peer.getPeerType().toApi(), peer.getPeerId()))) + ); + } } @@ -447,7 +479,9 @@ public void loadMoreArchivedDialogs(final boolean init, final RpcCallback getHistoryActor(peer).loadMore()); + if (peer.getPeerType() != PeerType.PRIVATE_ENCRYPTED) { + im.actor.runtime.Runtime.dispatch(() -> getHistoryActor(peer).loadMore()); + } } // diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/MessagesProcessorEncrypted.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/MessagesProcessorEncrypted.java new file mode 100644 index 0000000000..0d68d34fca --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/MessagesProcessorEncrypted.java @@ -0,0 +1,36 @@ +package im.actor.core.modules.messaging; + +import im.actor.core.api.ApiEncryptedContent; +import im.actor.core.api.ApiEncryptedDeleteAll; +import im.actor.core.api.ApiEncryptedDeleteContent; +import im.actor.core.api.ApiEncryptedEditContent; +import im.actor.core.api.ApiEncryptedMessageContent; +import im.actor.core.api.ApiEncryptedRead; +import im.actor.core.api.ApiEncryptedReceived; +import im.actor.core.modules.AbsModule; +import im.actor.core.modules.ModuleContext; +import im.actor.core.modules.encryption.updates.EncryptedSequenceProcessor; +import im.actor.runtime.actors.messages.Void; +import im.actor.runtime.promise.Promise; + +public class MessagesProcessorEncrypted extends AbsModule implements EncryptedSequenceProcessor { + + public MessagesProcessorEncrypted(ModuleContext context) { + super(context); + } + + @Override + public Promise onUpdate(int senderId, long date, ApiEncryptedContent update) { + + if (update instanceof ApiEncryptedMessageContent || + update instanceof ApiEncryptedReceived || + update instanceof ApiEncryptedRead || + update instanceof ApiEncryptedDeleteContent || + update instanceof ApiEncryptedEditContent || + update instanceof ApiEncryptedDeleteAll) { + return context().getMessagesModule().getRouter() + .onEncryptedUpdate(senderId, date, update); + } + return null; + } +} \ No newline at end of file diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/actions/CursorReaderActor.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/actions/CursorReaderActor.java index f2860e6e07..d91cc65100 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/actions/CursorReaderActor.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/actions/CursorReaderActor.java @@ -4,16 +4,22 @@ package im.actor.core.modules.messaging.actions; +import im.actor.core.api.ApiEncryptedRead; import im.actor.core.api.ApiOutPeer; import im.actor.core.api.rpc.RequestMessageRead; import im.actor.core.api.rpc.ResponseVoid; import im.actor.core.entity.Peer; +import im.actor.core.entity.PeerType; import im.actor.core.modules.ModuleContext; -import im.actor.core.network.RpcCallback; -import im.actor.core.network.RpcException; +import im.actor.core.util.RandomUtils; +import im.actor.runtime.actors.messages.Void; public class CursorReaderActor extends CursorActor { + // j2objc workaround + private static final ResponseVoid DUMB = null; + private static final Long DUMB2 = null; + public CursorReaderActor(ModuleContext context) { super(CURSOR_READ, context); } @@ -25,17 +31,20 @@ protected void perform(final Peer peer, final long date) { return; } - request(new RequestMessageRead(outPeer, date), new RpcCallback() { - @Override - public void onResult(ResponseVoid response) { + if (peer.getPeerType() == PeerType.PRIVATE_ENCRYPTED) { + context().getEncryption().doSend(RandomUtils.nextRid(), + new ApiEncryptedRead(peer.getPeerId(), date), peer.getPeerId()).then(r -> { onCompleted(peer, date); - } - - @Override - public void onError(RpcException e) { - CursorReaderActor.this.onError(peer, date); - } - }); + }).failure(e -> { + onError(peer, date); + }); + } else { + api(new RequestMessageRead(outPeer, date)).then(responseVoid -> { + onCompleted(peer, date); + }).failure(e -> { + onError(peer, date); + }); + } } // Messages diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/actions/CursorReceiverActor.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/actions/CursorReceiverActor.java index afca3ef43b..0ac0cc9762 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/actions/CursorReceiverActor.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/actions/CursorReceiverActor.java @@ -4,16 +4,21 @@ package im.actor.core.modules.messaging.actions; +import im.actor.core.api.ApiEncryptedReceived; import im.actor.core.api.ApiOutPeer; import im.actor.core.api.rpc.RequestMessageReceived; import im.actor.core.api.rpc.ResponseVoid; import im.actor.core.entity.Peer; +import im.actor.core.entity.PeerType; import im.actor.core.modules.ModuleContext; -import im.actor.core.network.RpcCallback; -import im.actor.core.network.RpcException; +import im.actor.core.util.RandomUtils; public class CursorReceiverActor extends CursorActor { + // j2objc workaround + private static final ResponseVoid DUMB = null; + private static final Long DUMB2 = null; + public CursorReceiverActor(ModuleContext context) { super(CURSOR_RECEIVED, context); } @@ -21,22 +26,24 @@ public CursorReceiverActor(ModuleContext context) { @Override protected void perform(final Peer peer, final long date) { ApiOutPeer outPeer = buidOutPeer(peer); - if (outPeer == null) { return; } - request(new RequestMessageReceived(outPeer, date), new RpcCallback() { - @Override - public void onResult(ResponseVoid response) { + if (peer.getPeerType() == PeerType.PRIVATE_ENCRYPTED) { + context().getEncryption().doSend(RandomUtils.nextRid(), + new ApiEncryptedReceived(peer.getPeerId(), date), peer.getPeerId()).then(r -> { onCompleted(peer, date); - } - - @Override - public void onError(RpcException e) { - CursorReceiverActor.this.onError(peer, date); - } - }); + }).failure(e -> { + onError(peer, date); + }); + } else { + api(new RequestMessageReceived(outPeer, date)).then(responseVoid -> { + onCompleted(peer, date); + }).failure(e -> { + onError(peer, date); + }); + } } @Override diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/actions/Destructor.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/actions/Destructor.java new file mode 100644 index 0000000000..0b0dabc78a --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/actions/Destructor.java @@ -0,0 +1,31 @@ +package im.actor.core.modules.messaging.actions; + +import java.util.List; + +import im.actor.core.entity.Peer; +import im.actor.core.modules.ModuleContext; +import im.actor.core.modules.messaging.actions.entity.MessageDesc; +import im.actor.runtime.actors.ActorInterface; +import im.actor.runtime.actors.messages.Void; +import im.actor.runtime.promise.Promise; + +import static im.actor.runtime.actors.ActorSystem.system; + +public class Destructor extends ActorInterface { + + public Destructor(ModuleContext context) { + super(system().actorOf("router/destructor", () -> new DestructorActor(context))); + } + + public Promise onMessages(Peer peer, List messages) { + return ask(new DestructorActor.NewMessages(peer, messages)); + } + + public Promise onMessageRead(Peer peer, long readDate) { + return ask(new DestructorActor.MessageRead(peer, readDate)); + } + + public Promise onMessageReadByMe(Peer peer, long readDate) { + return ask(new DestructorActor.MessageReadByMe(peer, readDate)); + } +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/actions/DestructorActor.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/actions/DestructorActor.java new file mode 100644 index 0000000000..6487e588b0 --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/actions/DestructorActor.java @@ -0,0 +1,278 @@ +package im.actor.core.modules.messaging.actions; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; + +import im.actor.core.entity.Peer; +import im.actor.core.modules.ModuleActor; +import im.actor.core.modules.ModuleContext; +import im.actor.core.modules.messaging.actions.entity.DestructPendingMessage; +import im.actor.core.modules.messaging.actions.entity.DestructPendingStorage; +import im.actor.core.modules.messaging.actions.entity.DestructQueueMessage; +import im.actor.core.modules.messaging.actions.entity.DestructQueueStorage; +import im.actor.core.modules.messaging.actions.entity.MessageDesc; +import im.actor.core.util.JavaUtil; +import im.actor.runtime.Runtime; +import im.actor.runtime.Storage; +import im.actor.runtime.actors.ActorCancellable; +import im.actor.runtime.actors.ask.AskMessage; +import im.actor.runtime.actors.messages.Void; +import im.actor.runtime.promise.Promise; +import im.actor.runtime.promise.PromisesArray; +import im.actor.runtime.storage.KeyValueStorage; + +public class DestructorActor extends ModuleActor { + + private static final int DEFAULT_DELAY = 15000; + + private KeyValueStorage keyValueStorage; + private DestructQueueStorage destructQueueStorage = new DestructQueueStorage(); + private ActorCancellable checkCancellable = null; + private boolean isDestructing = false; + + public DestructorActor(ModuleContext context) { + super(context); + } + + @Override + public void preStart() { + super.preStart(); + + keyValueStorage = Storage.createKeyValue("self_destruct"); + + // Load Destruction Queue + byte[] data = keyValueStorage.loadItem(0); + if (data != null) { + destructQueueStorage = DestructQueueStorage.fromBytes(data); + } + + scheduleCheck(); + } + + // + // Receiving messages + // + + public Promise onMessages(Peer peer, List messages) { + DestructPendingStorage storage = getStorage(peer); + if (storage == null) { + storage = new DestructPendingStorage(); + } + for (MessageDesc d : messages) { + DestructPendingMessage p = new DestructPendingMessage(d.getRid(), + d.getDate(), d.isOut(), d.getTimer()); + if (d.isNeedExplicitRead()) { + storage.getIndividualMessages().add(p); + } else { + storage.getMessages().add(p); + } + } + saveStorage(peer, storage); + return Promise.success(null); + } + + // + // Reading and starting self-destroy + // + + public Promise onMessageRead(Peer peer, long readDate) { + return onMessageRead(peer, readDate, true); + } + + public Promise onMessageReadByMe(Peer peer, long readDate) { + return onMessageRead(peer, readDate, false); + } + + public Promise onMessageRead(Peer peer, long readDate, boolean isOut) { + DestructPendingStorage pendingStorage = getStorage(peer); + if (pendingStorage != null) { + ArrayList queue = new ArrayList<>(); + for (DestructPendingMessage dp : pendingStorage.getMessages()) { + if (dp.isOut() == isOut && dp.getDate() <= readDate) { + queue.add(dp); + } + } + + if (queue.size() > 0) { + + // Adding to queue + for (DestructPendingMessage dp : queue) { + destructQueueStorage.getQueue().add(new DestructQueueMessage(peer, + dp.getRid(), dp.getTimer() + readDate)); + } + Collections.sort(destructQueueStorage.getQueue(), (destructQueueMessage, t1) -> { + return JavaUtil.compare(destructQueueMessage.getDestructDate(), t1.getDestructDate()); + }); + keyValueStorage.addOrUpdateItem(0, destructQueueStorage.toByteArray()); + + // Remove from pending + pendingStorage.getMessages().removeAll(queue); + saveStorage(peer, pendingStorage); + + // Checking queue + scheduleCheck(); + } + } + + return Promise.success(null); + } + + // + // Queue Checking + // + + private void scheduleCheck() { + if (checkCancellable != null) { + checkCancellable.cancel(); + checkCancellable = null; + } + if (isDestructing) { + return; + } + if (destructQueueStorage.getQueue().size() == 0) { + checkCancellable = schedule(new CheckQueue(), DEFAULT_DELAY); + } else { + long time = Runtime.getCurrentSyncedTime(); + long delta = destructQueueStorage.getQueue().get(0).getDestructDate() - time; + if (delta < 0) { + checkQueue(); + } else { + checkCancellable = schedule(new CheckQueue(), delta); + } + } + } + + private void checkQueue() { + if (isDestructing) { + return; + } + long time = Runtime.getCurrentSyncedTime(); + List pendingMessages = null; + for (DestructQueueMessage q : destructQueueStorage.getQueue()) { + if (q.getDestructDate() <= time) { + if (pendingMessages == null) { + pendingMessages = new ArrayList<>(); + } + pendingMessages.add(q); + } + } + if (pendingMessages != null) { + isDestructing = true; + HashMap> messages = new HashMap<>(); + for (DestructQueueMessage p : pendingMessages) { + if (!messages.containsKey(p.getPeer())) { + messages.put(p.getPeer(), new ArrayList<>()); + } + messages.get(p.getPeer()).add(p.getRid()); + } + + ArrayList> res = new ArrayList<>(); + for (Peer p : messages.keySet()) { + res.add(context().getMessagesModule().getRouter().onMessagesDestructed(p, + messages.get(p))); + } + final List finalPendingMessages = pendingMessages; + PromisesArray.ofPromises(res) + .zip(r -> null) + .then(r -> { + destructQueueStorage.getQueue().removeAll(finalPendingMessages); + keyValueStorage.addOrUpdateItem(0, destructQueueStorage.toByteArray()); + isDestructing = false; + scheduleCheck(); + }); + } else { + scheduleCheck(); + } + } + + // + // Tools + // + + private DestructPendingStorage getStorage(Peer peer) { + byte[] data = keyValueStorage.loadItem(peer.getUnuqueId()); + if (data == null) { + return null; + } + try { + return DestructPendingStorage.fromBytes(data); + } catch (IOException e) { + e.printStackTrace(); + return null; + } + } + + private void saveStorage(Peer peer, DestructPendingStorage storage) { + if (storage.getMessages().size() == 0 && storage.getIndividualMessages().size() == 0) { + keyValueStorage.removeItem(peer.getUnuqueId()); + } else { + keyValueStorage.addOrUpdateItem(peer.getUnuqueId(), storage.toByteArray()); + } + } + + // + // Messages + // + + @Override + public Promise onAsk(Object message) throws Exception { + if (message instanceof MessageRead) { + MessageRead messageRead = (MessageRead) message; + return onMessageRead(messageRead.peer, messageRead.readDate); + } else if (message instanceof MessageReadByMe) { + MessageReadByMe readByMe = (MessageReadByMe) message; + return onMessageReadByMe(readByMe.peer, readByMe.readDate); + } else if (message instanceof NewMessages) { + NewMessages newMessages = (NewMessages) message; + return onMessages(newMessages.peer, newMessages.messages); + } else { + return super.onAsk(message); + } + } + + @Override + public void onReceive(Object message) { + if (message instanceof CheckQueue) { + checkQueue(); + } else { + super.onReceive(message); + } + } + + public static class MessageRead implements AskMessage { + private final Peer peer; + private final long readDate; + + public MessageRead(Peer peer, long readDate) { + this.peer = peer; + this.readDate = readDate; + } + } + + public static class MessageReadByMe implements AskMessage { + private final Peer peer; + private final long readDate; + + public MessageReadByMe(Peer peer, long readDate) { + this.peer = peer; + this.readDate = readDate; + } + } + + public static class NewMessages implements AskMessage { + private final Peer peer; + private final List messages; + + public NewMessages(Peer peer, List messages) { + this.peer = peer; + this.messages = messages; + } + } + + private static class CheckQueue { + + } +} \ No newline at end of file diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/actions/MessageDeleteActor.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/actions/MessageDeleteActor.java index 6d27b10f5b..0059a7f373 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/actions/MessageDeleteActor.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/actions/MessageDeleteActor.java @@ -8,6 +8,7 @@ import java.util.ArrayList; import java.util.List; +import im.actor.core.api.ApiEncryptedDeleteContent; import im.actor.core.api.ApiPeer; import im.actor.core.api.ApiOutPeer; import im.actor.core.api.base.SeqUpdate; @@ -15,12 +16,14 @@ import im.actor.core.api.rpc.ResponseSeq; import im.actor.core.api.updates.UpdateMessageDelete; import im.actor.core.entity.Peer; +import im.actor.core.entity.PeerType; import im.actor.core.modules.ModuleContext; import im.actor.core.modules.messaging.actions.entity.Delete; import im.actor.core.modules.messaging.actions.entity.DeleteStorage; import im.actor.core.modules.ModuleActor; import im.actor.core.network.RpcCallback; import im.actor.core.network.RpcException; +import im.actor.core.util.RandomUtils; import im.actor.runtime.storage.SyncKeyValue; public class MessageDeleteActor extends ModuleActor { @@ -61,32 +64,33 @@ void saveStorage() { } public void performDelete(final Peer peer, final List rids) { - final ApiOutPeer outPeer = buidOutPeer(peer); - final ApiPeer apiPeer = buildApiPeer(peer); - request(new RequestDeleteMessage(outPeer, rids), new RpcCallback() { - - @Override - public void onResult(ResponseSeq response) { + if (peer.getPeerType() == PeerType.PRIVATE_ENCRYPTED) { + context().getEncryption().doSend(RandomUtils.nextRid(), + new ApiEncryptedDeleteContent(peer.getPeerId(), rids), peer.getPeerId()).then(r -> { + if (deleteStorage.getPendingDeletions().containsKey(peer)) { + deleteStorage.getPendingDeletions().get(peer).getRids().removeAll(rids); + saveStorage(); + } + }); + } else { + final ApiOutPeer outPeer = buidOutPeer(peer); + final ApiPeer apiPeer = buildApiPeer(peer); + api(new RequestDeleteMessage(outPeer, rids)).then(r -> { if (deleteStorage.getPendingDeletions().containsKey(peer)) { deleteStorage.getPendingDeletions().get(peer).getRids().removeAll(rids); saveStorage(); } - updates().onUpdateReceived(new SeqUpdate(response.getSeq(),response.getState(), - UpdateMessageDelete.HEADER,new UpdateMessageDelete(apiPeer, rids).toByteArray())); - } - - @Override - public void onError(RpcException e) { - - } - }); + updates().onUpdateReceived(new SeqUpdate(r.getSeq(), r.getState(), + UpdateMessageDelete.HEADER, new UpdateMessageDelete(apiPeer, rids).toByteArray())); + }); + } } public void onDeleteMessage(Peer peer, List rids) { // Add to storage if (!deleteStorage.getPendingDeletions().containsKey(peer)) { - deleteStorage.getPendingDeletions().put(peer, new Delete(peer,new ArrayList())); + deleteStorage.getPendingDeletions().put(peer, new Delete(peer, new ArrayList())); } deleteStorage.getPendingDeletions().get(peer).getRids().addAll(rids); saveStorage(); diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/actions/SenderActor.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/actions/SenderActor.java index 8deddc101d..3371b6383c 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/actions/SenderActor.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/actions/SenderActor.java @@ -12,8 +12,11 @@ import java.util.HashMap; import java.util.List; +import im.actor.core.api.ApiDocumentEncryptionInfo; import im.actor.core.api.ApiDocumentExAnimation; import im.actor.core.api.ApiDocumentExVoice; +import im.actor.core.api.ApiEncryptedContent; +import im.actor.core.api.ApiEncryptedMessageContent; import im.actor.core.api.ApiFastThumb; import im.actor.core.api.ApiJsonMessage; import im.actor.core.api.ApiMessage; @@ -96,15 +99,20 @@ public void preStart() { boolean isChanged = false; ArrayList messages = pendingMessages.getPendingMessages(); for (PendingMessage pending : messages.toArray(new PendingMessage[messages.size()])) { - if (pending.getContent() instanceof TextContent) { - performSendContent(pending.getPeer(), pending.getRid(), pending.getContent()); + if (pending.getContent() instanceof TextContent || + pending.getContent() instanceof JsonContent || + pending.getContent() instanceof StickerContent || + pending.getContent() instanceof LocationContent || + pending.getContent() instanceof ContactContent) { + performSendContent(pending.getPeer(), pending.getRid(), pending.getTimer(), pending.getContent()); } else if (pending.getContent() instanceof DocumentContent) { DocumentContent documentContent = (DocumentContent) pending.getContent(); if (documentContent.getSource() instanceof FileLocalSource) { if (Storage.isFsPersistent()) { performUploadFile(pending.getRid(), ((FileLocalSource) documentContent.getSource()).getFileDescriptor(), - ((FileLocalSource) documentContent.getSource()).getFileName()); + ((FileLocalSource) documentContent.getSource()).getFileName(), + pending.isEncryptedFile()); } else { List rids = new ArrayList<>(); rids.add(pending.getRid()); @@ -114,7 +122,7 @@ public void preStart() { } } else { performSendContent(pending.getPeer(), pending.getRid(), - pending.getContent()); + pending.getTimer(), pending.getContent()); } } } @@ -141,10 +149,6 @@ public void doSendText(@NotNull Peer peer, @NotNull String text, text = text.trim(); - long rid = RandomUtils.nextRid(); - long date = createPendingDate(); - long sortDate = date + 365 * 24 * 60 * 60 * 1000L; - if (autoDetect) { mentions = new ArrayList<>(); if (peer.getPeerType() == PeerType.GROUP) { @@ -171,192 +175,107 @@ public void doSendText(@NotNull Peer peer, @NotNull String text, TextContent content = TextContent.create(text, null, mentions); - Message message = new Message(rid, sortDate, date, myUid(), MessageState.PENDING, content); - - context().getMessagesModule().getRouter().onOutgoingMessage(peer, message); - - pendingMessages.getPendingMessages().add(new PendingMessage(peer, rid, content)); - savePending(); - - performSendContent(peer, rid, content); + PendingMessage pending = prepareSend(peer, content, false); + performSendContent(peer, pending.getRid(), pending.getTimer(), content); } public void doSendJson(Peer peer, JsonContent content) { - long rid = RandomUtils.nextRid(); - long date = createPendingDate(); - long sortDate = date + 365 * 24 * 60 * 60 * 1000L; - - Message message = new Message(rid, sortDate, date, myUid(), MessageState.PENDING, content); - context().getMessagesModule().getRouter().onOutgoingMessage(peer, message); - - pendingMessages.getPendingMessages().add(new PendingMessage(peer, rid, content)); - savePending(); - - performSendContent(peer, rid, content); + PendingMessage pending = prepareSend(peer, content, false); + performSendContent(peer, pending.getRid(), pending.getTimer(), content); } // Sending sticker public void doSendSticker(@NotNull Peer peer, @NotNull Sticker sticker) { - - long rid = RandomUtils.nextRid(); - long date = createPendingDate(); - long sortDate = date + 365 * 24 * 60 * 60 * 1000L; - StickerContent content = StickerContent.create(sticker); - - Message message = new Message(rid, sortDate, date, myUid(), MessageState.PENDING, content); - context().getMessagesModule().getRouter().onOutgoingMessage(peer, message); - - pendingMessages.getPendingMessages().add(new PendingMessage(peer, rid, content)); - savePending(); - - performSendContent(peer, rid, content); + PendingMessage pending = prepareSend(peer, content, false); + performSendContent(peer, pending.getRid(), pending.getTimer(), content); } public void doSendContact(@NotNull Peer peer, @NotNull ArrayList emails, @NotNull ArrayList phones, @Nullable String name, @Nullable String base64photo) { - - long rid = RandomUtils.nextRid(); - long date = createPendingDate(); - long sortDate = date + 365 * 24 * 60 * 60 * 1000L; - - ContactContent content = ContactContent.create(name, phones, emails, base64photo); - - Message message = new Message(rid, sortDate, date, myUid(), MessageState.PENDING, content); - context().getMessagesModule().getRouter().onOutgoingMessage(peer, message); - - pendingMessages.getPendingMessages().add(new PendingMessage(peer, rid, content)); - savePending(); - - performSendContent(peer, rid, content); + PendingMessage pending = prepareSend(peer, content, false); + performSendContent(peer, pending.getRid(), pending.getTimer(), content); } public void doSendLocation(@NotNull Peer peer, @NotNull Double longitude, @NotNull Double latitude, @Nullable String street, @Nullable String place) { - - long rid = RandomUtils.nextRid(); - long date = createPendingDate(); - long sortDate = date + 365 * 24 * 60 * 60 * 1000L; - - LocationContent content = LocationContent.create(longitude, latitude, street, place); - - Message message = new Message(rid, sortDate, date, myUid(), MessageState.PENDING, content); - context().getMessagesModule().getRouter().onOutgoingMessage(peer, message); - - pendingMessages.getPendingMessages().add(new PendingMessage(peer, rid, content)); - savePending(); - - performSendContent(peer, rid, content); + PendingMessage pending = prepareSend(peer, content, false); + performSendContent(peer, pending.getRid(), pending.getTimer(), content); } public void doForwardContent(Peer peer, AbsContent content) { - long rid = RandomUtils.nextRid(); - long date = createPendingDate(); - long sortDate = date + 365 * 24 * 60 * 60 * 1000L; - - Message message = new Message(rid, sortDate, date, myUid(), MessageState.PENDING, content); - context().getMessagesModule().getRouter().onOutgoingMessage(peer, message); - - pendingMessages.getPendingMessages().add(new PendingMessage(peer, rid, content)); - savePending(); - - performSendContent(peer, rid, content); + PendingMessage pending = prepareSend(peer, content, false); + performSendContent(peer, pending.getRid(), pending.getTimer(), content); } // Sending documents public void doSendDocument(Peer peer, String fileName, String mimeType, int fileSize, FastThumb fastThumb, String descriptor) { - long rid = RandomUtils.nextRid(); - long date = createPendingDate(); - long sortDate = date + 365 * 24 * 60 * 60 * 1000L; DocumentContent documentContent = DocumentContent.createLocal(fileName, fileSize, descriptor, mimeType, fastThumb); - - Message message = new Message(rid, sortDate, date, myUid(), MessageState.PENDING, documentContent); - context().getMessagesModule().getRouter().onOutgoingMessage(peer, message); - - pendingMessages.getPendingMessages().add(new PendingMessage(peer, rid, documentContent)); - savePending(); - - performUploadFile(rid, descriptor, fileName); + PendingMessage pending = prepareSend(peer, documentContent, true); + performUploadFile(pending.getRid(), descriptor, fileName, pending.isEncryptedFile()); } public void doSendPhoto(Peer peer, FastThumb fastThumb, String descriptor, String fileName, int fileSize, int w, int h) { - long rid = RandomUtils.nextRid(); - long date = createPendingDate(); - long sortDate = date + 365 * 24 * 60 * 60 * 1000L; PhotoContent photoContent = PhotoContent.createLocalPhoto(descriptor, fileName, fileSize, w, h, fastThumb); - - Message message = new Message(rid, sortDate, date, myUid(), MessageState.PENDING, photoContent); - context().getMessagesModule().getRouter().onOutgoingMessage(peer, message); - - pendingMessages.getPendingMessages().add(new PendingMessage(peer, rid, photoContent)); - savePending(); - - performUploadFile(rid, descriptor, fileName); + PendingMessage pending = prepareSend(peer, photoContent, true); + performUploadFile(pending.getRid(), descriptor, fileName, pending.isEncryptedFile()); } public void doSendAudio(Peer peer, String descriptor, String fileName, int fileSize, int duration) { - long rid = RandomUtils.nextRid(); - long date = createPendingDate(); - long sortDate = date + 365 * 24 * 60 * 60 * 1000L; VoiceContent audioContent = VoiceContent.createLocalAudio(descriptor, fileName, fileSize, duration); - - Message message = new Message(rid, sortDate, date, myUid(), MessageState.PENDING, audioContent); - context().getMessagesModule().getRouter().onOutgoingMessage(peer, message); - - pendingMessages.getPendingMessages().add(new PendingMessage(peer, rid, audioContent)); - savePending(); - - performUploadFile(rid, descriptor, fileName); + PendingMessage pending = prepareSend(peer, audioContent, true); + performUploadFile(pending.getRid(), descriptor, fileName, pending.isEncryptedFile()); } public void doSendVideo(Peer peer, String fileName, int w, int h, int duration, FastThumb fastThumb, String descriptor, int fileSize) { - long rid = RandomUtils.nextRid(); - long date = createPendingDate(); - long sortDate = date + 365 * 24 * 60 * 60 * 1000L; VideoContent videoContent = VideoContent.createLocalVideo(descriptor, fileName, fileSize, w, h, duration, fastThumb); - - Message message = new Message(rid, sortDate, date, myUid(), MessageState.PENDING, videoContent); - context().getMessagesModule().getRouter().onOutgoingMessage(peer, message); - - pendingMessages.getPendingMessages().add(new PendingMessage(peer, rid, videoContent)); - savePending(); - - performUploadFile(rid, descriptor, fileName); + PendingMessage pending = prepareSend(peer, videoContent, true); + performUploadFile(pending.getRid(), descriptor, fileName, pending.isEncryptedFile()); } public void doSendAnimation(Peer peer, String fileName, int w, int h, FastThumb fastThumb, String descriptor, int fileSize) { + AnimationContent animationContent = AnimationContent.createLocalAnimation(descriptor, + fileName, fileSize, w, h, fastThumb); + PendingMessage pending = prepareSend(peer, animationContent, true); + performUploadFile(pending.getRid(), descriptor, fileName, pending.isEncryptedFile()); + } + + private PendingMessage prepareSend(Peer peer, AbsContent content, boolean isFile) { long rid = RandomUtils.nextRid(); long date = createPendingDate(); long sortDate = date + 365 * 24 * 60 * 60 * 1000L; - AnimationContent animationContent = AnimationContent.createLocalAnimation(descriptor, - fileName, fileSize, w, h, fastThumb); + int timer = 0; + if (peer.getPeerType() == PeerType.PRIVATE_ENCRYPTED) { + timer = context().getEncryption().getConversationState().get(peer.getPeerId()).getTimer().get(); + } - Message message = new Message(rid, sortDate, date, myUid(), MessageState.PENDING, animationContent); + Message message = new Message(rid, sortDate, date, myUid(), MessageState.PENDING, content, + new ArrayList<>(), 0, timer); context().getMessagesModule().getRouter().onOutgoingMessage(peer, message); - - pendingMessages.getPendingMessages().add(new PendingMessage(peer, rid, animationContent)); + boolean isEncrypted = peer.getPeerType() == PeerType.PRIVATE_ENCRYPTED && isFile; + PendingMessage pendingMessage = new PendingMessage(peer, rid, content, timer, isEncrypted); + pendingMessages.getPendingMessages().add(pendingMessage); savePending(); - - performUploadFile(rid, descriptor, fileName); + return pendingMessage; } - private void performUploadFile(long rid, String descriptor, String fileName) { + private void performUploadFile(long rid, String descriptor, String fileName, boolean encrypt) { fileUplaodingWakeLocks.put(rid, Runtime.makeWakeLock()); - context().getFilesModule().requestUpload(rid, descriptor, fileName, self()); + context().getFilesModule().requestUpload(rid, descriptor, fileName, encrypt, self()); } private void onFileUploaded(long rid, FileReference fileReference) { @@ -391,9 +310,10 @@ private void onFileUploaded(long rid, FileReference fileReference) { return; } - pendingMessages.getPendingMessages().add(new PendingMessage(msg.getPeer(), msg.getRid(), nContent)); + pendingMessages.getPendingMessages().add(new PendingMessage(msg.getPeer(), msg.getRid(), + nContent, msg.getTimer(), msg.isEncryptedFile())); context().getMessagesModule().getRouter().onContentChanged(msg.getPeer(), msg.getRid(), nContent); - performSendContent(msg.getPeer(), rid, nContent); + performSendContent(msg.getPeer(), rid, msg.getTimer(), nContent); fileUplaodingWakeLocks.remove(rid).releaseLock(); } @@ -409,7 +329,7 @@ private void onFileUploadError(long rid) { // Sending content - private void performSendContent(final Peer peer, final long rid, AbsContent content) { + private void performSendContent(final Peer peer, final long rid, int timer, AbsContent content) { WakeLock wakeLock = im.actor.runtime.Runtime.makeWakeLock(); ApiMessage message; @@ -421,7 +341,6 @@ private void performSendContent(final Peer peer, final long rid, AbsContent cont FileRemoteSource source = (FileRemoteSource) documentContent.getSource(); ApiDocumentEx documentEx = null; - if (content instanceof PhotoContent) { PhotoContent photoContent = (PhotoContent) content; documentEx = new ApiDocumentExPhoto(photoContent.getW(), photoContent.getH()); @@ -436,7 +355,6 @@ private void performSendContent(final Peer peer, final long rid, AbsContent cont documentEx = new ApiDocumentExVoice(voiceContent.getDuration()); } - ApiFastThumb fastThumb = null; if (documentContent.getFastThumb() != null) { fastThumb = new ApiFastThumb( @@ -445,12 +363,16 @@ private void performSendContent(final Peer peer, final long rid, AbsContent cont documentContent.getFastThumb().getImage()); } + ApiDocumentEncryptionInfo apiEncryptionInfo = + source.getFileReference().getEncryptionInfo(); + message = new ApiDocumentMessage(source.getFileReference().getFileId(), source.getFileReference().getAccessHash(), source.getFileReference().getFileSize(), source.getFileReference().getFileName(), documentContent.getMimeType(), - fastThumb, documentEx); + fastThumb, documentEx, + apiEncryptionInfo); } else if (content instanceof LocationContent) { message = new ApiJsonMessage(((LocationContent) content).getRawJson()); } else if (content instanceof ContactContent) { @@ -463,33 +385,46 @@ private void performSendContent(final Peer peer, final long rid, AbsContent cont return; } - performSendApiContent(peer, rid, message, wakeLock); + performSendApiContent(peer, rid, message, timer, wakeLock); } - private void performSendApiContent(final Peer peer, final long rid, ApiMessage message, final WakeLock wakeLock) { - final ApiOutPeer outPeer = buidOutPeer(peer); - final ApiPeer apiPeer = buildApiPeer(peer); - if (outPeer == null || apiPeer == null) { - return; - } - request(new RequestSendMessage(outPeer, rid, message, null, null), - new RpcCallback() { - @Override - public void onResult(ResponseSeqDate response) { - self().send(new MessageSent(peer, rid)); - updates().onUpdateReceived(new SeqUpdate(response.getSeq(), - response.getState(), - UpdateMessageSent.HEADER, - new UpdateMessageSent(apiPeer, rid, response.getDate()).toByteArray())); - wakeLock.releaseLock(); - } + private void performSendApiContent(final Peer peer, final long rid, ApiMessage message, int timer, final WakeLock wakeLock) { + if (peer.getPeerType() == PeerType.PRIVATE || peer.getPeerType() == PeerType.GROUP) { + final ApiOutPeer outPeer = buidOutPeer(peer); + final ApiPeer apiPeer = buildApiPeer(peer); + request(new RequestSendMessage(outPeer, rid, message, null, null), + new RpcCallback() { + @Override + public void onResult(ResponseSeqDate response) { + self().send(new MessageSent(peer, rid)); + updates().onUpdateReceived(new SeqUpdate(response.getSeq(), + response.getState(), + UpdateMessageSent.HEADER, + new UpdateMessageSent(apiPeer, rid, response.getDate()).toByteArray())); + wakeLock.releaseLock(); + } - @Override - public void onError(RpcException e) { - self().send(new MessageError(peer, rid)); - wakeLock.releaseLock(); - } - }); + @Override + public void onError(RpcException e) { + self().send(new MessageError(peer, rid)); + wakeLock.releaseLock(); + } + }); + } else if (peer.getPeerType() == PeerType.PRIVATE_ENCRYPTED) { + Log.d("SenderActor", "Pending encrypted message: " + message); + Integer sTimer = null; + if (timer > 0) { + sTimer = timer; + } + ApiEncryptedContent content = new ApiEncryptedMessageContent(peer.getPeerId(), + rid, message, sTimer); + + context().getEncryption().doSend(rid, content, peer.getPeerId()) + .chain(r -> context().getMessagesModule().getRouter().onOutgoingSent(peer, rid, r)) + .then(r -> self().send(new MessageSent(peer, rid))) + .failure(e -> self().send(new MessageError(peer, rid))) + .after((r, e) -> wakeLock.releaseLock()); + } } private void onSent(Peer peer, long rid) { diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/actions/entity/DestructPendingMessage.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/actions/entity/DestructPendingMessage.java new file mode 100644 index 0000000000..0bb9df405a --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/actions/entity/DestructPendingMessage.java @@ -0,0 +1,63 @@ +package im.actor.core.modules.messaging.actions.entity; + +import java.io.IOException; + +import im.actor.runtime.bser.Bser; +import im.actor.runtime.bser.BserObject; +import im.actor.runtime.bser.BserValues; +import im.actor.runtime.bser.BserWriter; + +public class DestructPendingMessage extends BserObject { + + public static DestructPendingMessage fromBytes(byte[] data) throws IOException { + return Bser.parse(new DestructPendingMessage(), data); + } + + private long rid; + private long date; + private boolean isOut; + private int timer; + + public DestructPendingMessage(long rid, long date, boolean isOut, int timer) { + this.rid = rid; + this.date = date; + this.isOut = isOut; + this.timer = timer; + } + + private DestructPendingMessage() { + + } + + public long getRid() { + return rid; + } + + public long getDate() { + return date; + } + + public boolean isOut() { + return isOut; + } + + public int getTimer() { + return timer; + } + + @Override + public void parse(BserValues values) throws IOException { + rid = values.getLong(1); + date = values.getLong(2); + isOut = values.getBool(3); + timer = values.getInt(4); + } + + @Override + public void serialize(BserWriter writer) throws IOException { + writer.writeLong(1, rid); + writer.writeLong(2, date); + writer.writeBool(3, isOut); + writer.writeInt(4, timer); + } +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/actions/entity/DestructPendingStorage.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/actions/entity/DestructPendingStorage.java new file mode 100644 index 0000000000..7c0fcb2341 --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/actions/entity/DestructPendingStorage.java @@ -0,0 +1,56 @@ +package im.actor.core.modules.messaging.actions.entity; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import im.actor.runtime.bser.Bser; +import im.actor.runtime.bser.BserObject; +import im.actor.runtime.bser.BserValues; +import im.actor.runtime.bser.BserWriter; + +public class DestructPendingStorage extends BserObject { + + public static DestructPendingStorage fromBytes(byte[] data) throws IOException { + return Bser.parse(new DestructPendingStorage(), data); + } + + private List individualMessages; + private List messages; + + public DestructPendingStorage(List individualMessages, List messages) { + this.individualMessages = individualMessages; + this.messages = messages; + } + + public DestructPendingStorage() { + this.individualMessages = new ArrayList<>(); + this.messages = new ArrayList<>(); + } + + public List getIndividualMessages() { + return individualMessages; + } + + public List getMessages() { + return messages; + } + + @Override + public void parse(BserValues values) throws IOException { + individualMessages.clear(); + for (byte[] b : values.getRepeatedBytes(1)) { + individualMessages.add(DestructPendingMessage.fromBytes(b)); + } + messages.clear(); + for (byte[] b : values.getRepeatedBytes(2)) { + messages.add(DestructPendingMessage.fromBytes(b)); + } + } + + @Override + public void serialize(BserWriter writer) throws IOException { + writer.writeRepeatedObj(1, individualMessages); + writer.writeRepeatedObj(2, messages); + } +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/actions/entity/DestructQueueMessage.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/actions/entity/DestructQueueMessage.java new file mode 100644 index 0000000000..0b1ca4926c --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/actions/entity/DestructQueueMessage.java @@ -0,0 +1,56 @@ +package im.actor.core.modules.messaging.actions.entity; + +import java.io.IOException; + +import im.actor.core.entity.Peer; +import im.actor.runtime.bser.Bser; +import im.actor.runtime.bser.BserObject; +import im.actor.runtime.bser.BserValues; +import im.actor.runtime.bser.BserWriter; + +public class DestructQueueMessage extends BserObject { + + public static DestructQueueMessage fromBytes(byte[] data) throws IOException { + return Bser.parse(new DestructQueueMessage(), data); + } + + private Peer peer; + private long rid; + private long destructDate; + + public DestructQueueMessage(Peer peer, long rid, long destructDate) { + this.peer = peer; + this.rid = rid; + this.destructDate = destructDate; + } + + private DestructQueueMessage() { + + } + + public Peer getPeer() { + return peer; + } + + public long getRid() { + return rid; + } + + public long getDestructDate() { + return destructDate; + } + + @Override + public void parse(BserValues values) throws IOException { + peer = Peer.fromUniqueId(values.getLong(1)); + rid = values.getLong(2); + destructDate = values.getLong(3); + } + + @Override + public void serialize(BserWriter writer) throws IOException { + writer.writeLong(1, peer.getUnuqueId()); + writer.writeLong(2, rid); + writer.writeLong(3, destructDate); + } +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/actions/entity/DestructQueueStorage.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/actions/entity/DestructQueueStorage.java new file mode 100644 index 0000000000..72b2655250 --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/actions/entity/DestructQueueStorage.java @@ -0,0 +1,48 @@ +package im.actor.core.modules.messaging.actions.entity; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import im.actor.runtime.bser.Bser; +import im.actor.runtime.bser.BserObject; +import im.actor.runtime.bser.BserValues; +import im.actor.runtime.bser.BserWriter; + +public class DestructQueueStorage extends BserObject { + + public static DestructQueueStorage fromBytes(byte[] data) { + try { + return Bser.parse(new DestructQueueStorage(), data); + } catch (IOException e) { + e.printStackTrace(); + return new DestructQueueStorage(); + } + } + + private List queue; + + public DestructQueueStorage(List queue) { + this.queue = queue; + } + + public DestructQueueStorage() { + this.queue = new ArrayList<>(); + } + + public List getQueue() { + return queue; + } + + @Override + public void parse(BserValues values) throws IOException { + for (byte[] b : values.getRepeatedBytes(1)) { + queue.add(DestructQueueMessage.fromBytes(b)); + } + } + + @Override + public void serialize(BserWriter writer) throws IOException { + writer.writeRepeatedObj(1, queue); + } +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/actions/entity/MessageDesc.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/actions/entity/MessageDesc.java new file mode 100644 index 0000000000..4fe5112974 --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/actions/entity/MessageDesc.java @@ -0,0 +1,38 @@ +package im.actor.core.modules.messaging.actions.entity; + +public class MessageDesc { + + private long rid; + private boolean isOut; + private long date; + private int timer; + private boolean isNeedExplicitRead; + + public MessageDesc(long rid, boolean isOut, long date, int timer, boolean isNeedExplicitRead) { + this.rid = rid; + this.isOut = isOut; + this.date = date; + this.timer = timer; + this.isNeedExplicitRead = isNeedExplicitRead; + } + + public long getRid() { + return rid; + } + + public boolean isOut() { + return isOut; + } + + public long getDate() { + return date; + } + + public int getTimer() { + return timer; + } + + public boolean isNeedExplicitRead() { + return isNeedExplicitRead; + } +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/actions/entity/PendingMessage.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/actions/entity/PendingMessage.java index 27ae56950a..3097c65261 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/actions/entity/PendingMessage.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/actions/entity/PendingMessage.java @@ -23,11 +23,15 @@ public static PendingMessage fromBytes(byte[] data) throws IOException { private long rid; private AbsContent content; private boolean isError; + private int timer; + private boolean isEncryptedFile; - public PendingMessage(Peer peer, long rid, AbsContent content) { + public PendingMessage(Peer peer, long rid, AbsContent content, int timer, boolean isEncryptedFile) { this.peer = peer; this.rid = rid; this.content = content; + this.timer = timer; + this.isEncryptedFile = isEncryptedFile; } private PendingMessage() { @@ -50,12 +54,22 @@ public boolean isError() { return isError; } + public int getTimer() { + return timer; + } + + public boolean isEncryptedFile() { + return isEncryptedFile; + } + @Override public void parse(BserValues values) throws IOException { peer = Peer.fromUniqueId(values.getLong(1)); rid = values.getLong(2); content = AbsContent.parse(values.getBytes(3)); isError = values.getBool(4, false); + timer = values.getInt(5, 0); + isEncryptedFile = values.getBool(7, false); } @Override @@ -64,5 +78,7 @@ public void serialize(BserWriter writer) throws IOException { writer.writeLong(2, rid); writer.writeBytes(3, AbsContent.serialize(content)); writer.writeBool(4, isError); + writer.writeInt(5, timer); + writer.writeBool(7, isEncryptedFile); } } diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/dialogs/DialogsActor.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/dialogs/DialogsActor.java index ef3e642582..6f9349b529 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/dialogs/DialogsActor.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/dialogs/DialogsActor.java @@ -16,6 +16,7 @@ import im.actor.core.entity.GroupType; import im.actor.core.entity.Message; import im.actor.core.entity.Peer; +import im.actor.core.entity.PeerType; import im.actor.core.entity.User; import im.actor.core.entity.content.AbsContent; import im.actor.core.modules.ModuleContext; @@ -154,6 +155,8 @@ && equalsE(dialog.getDialogAvatar(), user.getAvatar())) { Dialog updated = dialog.editPeerInfo(user.getName(), user.getAvatar()); addOrUpdateItem(updated); updateSearch(updated); + + // TODO: Update for secret chats } return Promise.success(null); @@ -345,8 +348,9 @@ private void notifyState(boolean force) { private PeerDesc buildPeerDesc(Peer peer) { switch (peer.getPeerType()) { case PRIVATE: + case PRIVATE_ENCRYPTED: User u = getUser(peer.getPeerId()); - return new PeerDesc(u.getName(), u.getAvatar(), u.isBot(), false); + return new PeerDesc(u.getName(), u.getAvatar(), u.isBot(), peer.getPeerType() == PeerType.PRIVATE_ENCRYPTED); case GROUP: Group g = getGroup(peer.getPeerId()); return new PeerDesc(g.getTitle(), g.getAvatar(), false, g.getGroupType() == GroupType.CHANNEL); diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/history/ConversationHistoryActor.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/history/ConversationHistoryActor.java index 6ac9f1609d..c61e173a6c 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/history/ConversationHistoryActor.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/history/ConversationHistoryActor.java @@ -16,6 +16,7 @@ import im.actor.core.entity.Message; import im.actor.core.entity.MessageState; import im.actor.core.entity.Peer; +import im.actor.core.entity.PeerType; import im.actor.core.entity.Reaction; import im.actor.core.entity.content.AbsContent; import im.actor.core.modules.api.ApiSupportConfiguration; @@ -55,10 +56,15 @@ public ConversationHistoryActor(Peer peer, ModuleContext context) { @Override public void preStart() { super.preStart(); - historyMaxDate = preferences().getLong(KEY_LOADED_DATE, Long.MAX_VALUE); - historyLoaded = preferences().getBool(KEY_LOADED, false); - if (!preferences().getBool(KEY_LOADED_INIT, false)) { - self().send(new LoadMore()); + if (peer.getPeerType() == PeerType.PRIVATE_ENCRYPTED) { + historyMaxDate = 0; + historyLoaded = true; + } else { + historyMaxDate = preferences().getLong(KEY_LOADED_DATE, Long.MAX_VALUE); + historyLoaded = preferences().getBool(KEY_LOADED, false); + if (!preferences().getBool(KEY_LOADED_INIT, false)) { + self().send(new LoadMore()); + } } } @@ -117,7 +123,7 @@ private Promise applyHistory(Peer peer, List history) } messages.add(new Message(historyMessage.getRid(), historyMessage.getDate(), historyMessage.getDate(), historyMessage.getSenderUid(), - state, content, reactions, 0)); + state, content, reactions, 0, 0)); maxLoadedDate = Math.min(historyMessage.getDate(), maxLoadedDate); if (historyMessage.getState() == ApiMessageState.RECEIVED) { diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/router/RouterActor.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/router/RouterActor.java index b711a0dfe2..b5df94f98e 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/router/RouterActor.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/router/RouterActor.java @@ -7,6 +7,13 @@ import im.actor.core.api.ApiDialogGroup; import im.actor.core.api.ApiDialogShort; +import im.actor.core.api.ApiEncryptedContent; +import im.actor.core.api.ApiEncryptedDeleteAll; +import im.actor.core.api.ApiEncryptedDeleteContent; +import im.actor.core.api.ApiEncryptedEditContent; +import im.actor.core.api.ApiEncryptedMessageContent; +import im.actor.core.api.ApiEncryptedRead; +import im.actor.core.api.ApiEncryptedReceived; import im.actor.core.api.ApiMessageReaction; import im.actor.core.api.rpc.RequestLoadGroupedDialogs; import im.actor.core.api.updates.UpdateChatClear; @@ -24,6 +31,7 @@ import im.actor.core.entity.Avatar; import im.actor.core.entity.ContentDescription; import im.actor.core.entity.ConversationState; +import im.actor.core.entity.EncryptedConversationState; import im.actor.core.entity.Group; import im.actor.core.entity.Message; import im.actor.core.entity.MessageState; @@ -32,6 +40,7 @@ import im.actor.core.entity.Reaction; import im.actor.core.entity.User; import im.actor.core.entity.content.AbsContent; +import im.actor.core.entity.content.DocumentContent; import im.actor.core.entity.content.TextContent; import im.actor.core.modules.AbsModule; import im.actor.core.modules.ModuleActor; @@ -39,7 +48,9 @@ import im.actor.core.modules.api.ApiSupportConfiguration; import im.actor.core.modules.messaging.actions.CursorReaderActor; import im.actor.core.modules.messaging.actions.CursorReceiverActor; +import im.actor.core.modules.messaging.actions.Destructor; import im.actor.core.modules.messaging.actions.SenderActor; +import im.actor.core.modules.messaging.actions.entity.MessageDesc; import im.actor.core.modules.messaging.dialogs.DialogsInt; import im.actor.core.modules.messaging.history.entity.DialogHistory; import im.actor.core.modules.messaging.router.entity.ActiveDialogGroup; @@ -54,14 +65,18 @@ import im.actor.core.modules.messaging.router.entity.RouterDeletedMessages; import im.actor.core.modules.messaging.router.entity.RouterDifferenceEnd; import im.actor.core.modules.messaging.router.entity.RouterDifferenceStart; +import im.actor.core.modules.messaging.router.entity.RouterEncryptedUpdate; import im.actor.core.modules.messaging.router.entity.RouterMessageOnlyActive; import im.actor.core.modules.messaging.router.entity.RouterMessageUpdate; +import im.actor.core.modules.messaging.router.entity.RouterMessagesSelfDestructed; import im.actor.core.modules.messaging.router.entity.RouterNewMessages; import im.actor.core.modules.messaging.router.entity.RouterOutgoingError; import im.actor.core.modules.messaging.router.entity.RouterOutgoingMessage; import im.actor.core.modules.messaging.router.entity.RouterOutgoingSent; import im.actor.core.modules.messaging.router.entity.RouterPeersChanged; import im.actor.core.modules.messaging.router.entity.RouterResetChat; +import im.actor.core.modules.messaging.router.entity.RouterSecretChatCleared; +import im.actor.core.modules.messaging.router.entity.RouterSecretChatDeleted; import im.actor.core.network.parser.Update; import im.actor.core.util.JavaUtil; import im.actor.core.viewmodel.DialogGroup; @@ -89,9 +104,14 @@ public class RouterActor extends ModuleActor { // Storage private KeyValueEngine conversationStates; + private KeyValueEngine encryptedConversationStates; // Active Dialogs private ActiveDialogStorage activeDialogStorage; + private ActiveDialogGroup activeEncryptedDialogGroupStorage; + + // Self-Destructor + private Destructor destructor; public RouterActor(ModuleContext context) { @@ -103,6 +123,13 @@ public void preStart() { super.preStart(); conversationStates = context().getMessagesModule().getConversationStates().getEngine(); + encryptedConversationStates = context().getEncryption().getConversationState().getEngine(); + + + // + // Self-Destructor + // + destructor = new Destructor(context()); // // Loading Active Dialogs @@ -116,6 +143,17 @@ public void preStart() { e.printStackTrace(); } } + + activeEncryptedDialogGroupStorage = new ActiveDialogGroup("EncryptedStuff", "Encrypted", new ArrayList<>()); + byte[] groupData = context().getStorageModule().getBlobStorage().loadItem(AbsModule.BLOB_ENCTYPTED_DIALOGS_ACTIVE_GROUP); + if (groupData != null) { + try { + activeEncryptedDialogGroupStorage = new ActiveDialogGroup(groupData); + } catch (IOException e) { + e.printStackTrace(); + } + } + if (!activeDialogStorage.isLoaded()) { api(new RequestLoadGroupedDialogs(ApiSupportConfiguration.OPTIMIZATIONS)) .chain(r -> updates().applyRelatedData(r.getUsers(), r.getGroups())) @@ -247,6 +285,38 @@ private Promise onNewMessages(Peer peer, List messages) { // // Updating Counter // + + boolean needNotifyActiveDialogsVM = false; + + boolean encrypted = peer.getPeerType() == PeerType.PRIVATE_ENCRYPTED; + EncryptedConversationState encryptedConversationState = encryptedConversationStates.getValue(peer.getPeerId()); + + if (encrypted && !isConversationVisible) { + + // Add this encrypted dialog to active if we don't have it there already + if (!activeEncryptedDialogGroupStorage.getPeers().contains(peer)) { + activeEncryptedDialogGroupStorage.getPeers().add(peer); + context().getStorageModule().getBlobStorage() + .addOrUpdateItem(AbsModule.BLOB_ENCTYPTED_DIALOGS_ACTIVE_GROUP, activeEncryptedDialogGroupStorage.toByteArray()); + } + + // update counter + ArrayList incoming = new ArrayList<>(); + for (Message m : messages) { + + if (m.getSenderId() != myUid()) { + incoming.add(new EncryptedConversationState.ShortMessage(m.getSortDate(), m.getRid())); + } + } + + encryptedConversationState = encryptedConversationState.addUnreadMessages(incoming); + + encryptedConversationStates.addOrUpdateItem(encryptedConversationState); + + needNotifyActiveDialogsVM = true; + } + + boolean isRead = false; if (unreadCount != 0) { if (isConversationVisible) { @@ -283,10 +353,14 @@ private Promise onNewMessages(Peer peer, List messages) { } conversationStates.addOrUpdateItem(state); - notifyActiveDialogsVM(); + needNotifyActiveDialogsVM = true; + } } + if (needNotifyActiveDialogsVM) { + notifyActiveDialogsVM(); + } // // Marking As Received @@ -300,7 +374,28 @@ private Promise onNewMessages(Peer peer, List messages) { // // Updating Dialog List // - Promise res = getDialogsRouter().onMessage(peer, topMessage, state.getUnreadCount()); + Promise res = getDialogsRouter().onMessage(peer, topMessage, encrypted ? encryptedConversationState.getUnreadCount() : state.getUnreadCount()); + + + // + // Update Self-Destructor + // + if (encrypted) { + ArrayList pendingDescs = new ArrayList<>(); + for (Message m : messages) { + if (m.getTimer() > 0) { + pendingDescs.add(new MessageDesc(m.getRid(), m.getSenderId() == myUid(), + m.getSortDate(), m.getTimer(), m.getContent() instanceof DocumentContent)); + } + } + if (pendingDescs.size() > 0) { + res = res.chain(r -> destructor.onMessages(peer, pendingDescs)); + if (isRead) { + long readDate = state.getInReadDate(); + res = res.chain(r -> destructor.onMessageReadByMe(peer, readDate)); + } + } + } // @@ -324,12 +419,19 @@ private Promise onNewMessages(Peer peer, List messages) { messagesCount += activeDialogueUnreadCount; } } + for (Peer activePeer : activeEncryptedDialogGroupStorage.getPeers()) { + int activeDialogueUnreadCount = encryptedConversationStates.getValue(activePeer.getPeerId()).getUnreadCount(); + if (activeDialogueUnreadCount > 0) { + dialogsCount++; + messagesCount += activeDialogueUnreadCount; + } + } context().getNotificationsModule().onInMessage( peer, m.getSenderId(), m.getSortDate(), - ContentDescription.fromContent(m.getContent()), + ContentDescription.fromContent(peer.getPeerType() == PeerType.PRIVATE_ENCRYPTED ? TextContent.create("new secret message", null, null) : m.getContent()), hasCurrentMention, messagesCount, dialogsCount); @@ -368,7 +470,18 @@ private Promise onOutgoingSent(Peer peer, long rid, long date) { updateChatState(peer); // Notify dialogs - return getDialogsRouter().onMessage(peer, updatedMsg, -1); + Promise res = getDialogsRouter().onMessage(peer, updatedMsg, -1); + + // Self-Destruct + if (peer.getPeerType() == PeerType.PRIVATE_ENCRYPTED && updatedMsg.getTimer() > 0) { + MessageDesc desc = new MessageDesc(rid, true, date, updatedMsg.getTimer(), + msg.getContent() instanceof DocumentContent); + ArrayList descs = new ArrayList<>(); + descs.add(desc); + res = res.chain(r -> destructor.onMessages(peer, descs)); + } + + return res; } else { return Promise.success(null); } @@ -529,9 +642,22 @@ private Promise onMessageDeleted(Peer peer, List rids) { } } + if (peer.getPeerType() == PeerType.PRIVATE_ENCRYPTED) { + encryptedConversationStates.addOrUpdateItem(encryptedConversationStates.getValue(peer.getPeerId()).read(rids)); + notifyActiveDialogsVM(); + } + return getDialogsRouter().onMessageDeleted(peer, head); } + private Promise onMessageDestructed(Peer peer, List rids) { + if (peer.getPeerType() == PeerType.PRIVATE_ENCRYPTED) { + encryptedConversationStates.addOrUpdateItem(encryptedConversationStates.getValue(peer.getPeerId()).read(rids)); + notifyActiveDialogsVM(); + } + return onMessageDeleted(peer, rids); + } + private Promise onChatClear(Peer peer) { conversation(peer).clear(); @@ -542,39 +668,52 @@ private Promise onChatClear(Peer peer) { conversationStates.addOrUpdateItem(state); } + if (peer.getPeerType() == PeerType.PRIVATE_ENCRYPTED) { + encryptedConversationStates.addOrUpdateItem(encryptedConversationStates.getValue(peer.getPeerId()).readAll()); + notifyActiveDialogsVM(); + } + updateChatState(peer); return getDialogsRouter().onChatClear(peer); } - private Promise onChatDropCache(Peer peer) { - return context().getMessagesModule().getHistoryActor(peer).reset(); - } - - private Promise onChatReset(Peer peer) { - - Log.d(TAG, "onChatReset"); + private Promise onChatDelete(Peer peer) { conversation(peer).clear(); ConversationState state = conversationStates.getValue(peer.getUnuqueId()); - state = state.changeIsLoaded(false); - conversationStates.addOrUpdateItem(state); + if (!state.isLoaded()) { + state = state.changeIsLoaded(true); + conversationStates.addOrUpdateItem(state); + } + + if (peer.getPeerType() == PeerType.PRIVATE_ENCRYPTED) { + encryptedConversationStates.addOrUpdateItem(encryptedConversationStates.getValue(peer.getPeerId()).readAll()); + notifyActiveDialogsVM(); + } updateChatState(peer); - return Promise.success(null); + return getDialogsRouter().onChatDelete(peer); } - private Promise onChatDelete(Peer peer) { + + // + // Cache invalidation + // + + private Promise onChatDropCache(Peer peer) { + return context().getMessagesModule().getHistoryActor(peer).reset(); + } + + private Promise onChatReset(Peer peer) { conversation(peer).clear(); ConversationState state = conversationStates.getValue(peer.getUnuqueId()); - if (!state.isLoaded()) { - state = state.changeIsLoaded(true); - conversationStates.addOrUpdateItem(state); - } + state = state.changeIsLoaded(false); + conversationStates.addOrUpdateItem(state); updateChatState(peer); @@ -593,6 +732,11 @@ private Promise onMessageRead(Peer peer, long date) { if (date > state.getOutReadDate()) { state = state.changeOutReadDate(date); res = getDialogsRouter().onPeerReadChanged(peer, date); + + if (peer.getPeerType() == PeerType.PRIVATE_ENCRYPTED) { + res = res.chain(r -> destructor.onMessageRead(peer, date)); + } + isChanged = true; } else { res = Promise.success(null); @@ -622,8 +766,14 @@ private Promise onMessageReceived(Peer peer, long date) { private Promise onMessageReadByMe(Peer peer, long date, int counter) { + boolean encrypted = peer.getPeerType() == PeerType.PRIVATE_ENCRYPTED; + ConversationState state = conversationStates.getValue(peer.getUnuqueId()); + if (encrypted) { + encryptedConversationStates.addOrUpdateItem(encryptedConversationStates.getValue(peer.getPeerId()).readBefore(date)); + } + if (state.getInReadDate() >= date) { return Promise.success(null); } @@ -635,6 +785,10 @@ private Promise onMessageReadByMe(Peer peer, long date, int counter) { Promise res = getDialogsRouter().onCounterChanged(peer, counter); + if (peer.getPeerType() == PeerType.PRIVATE_ENCRYPTED) { + res = res.chain(r -> destructor.onMessageReadByMe(peer, date)); + } + notifyActiveDialogsVM(); context().getNotificationsModule().onOwnRead(peer, date); @@ -721,6 +875,7 @@ private void markAsReadIfNeeded(Peer peer) { ConversationState state = conversationStates.getValue(peer.getUnuqueId()); long inMaxMessageDate = state.getInMaxMessageDate(); //check UnreadCount for zero, because it can be loaded from server (after login) + boolean needUpdateDialogs = false; if (state.getUnreadCount() != 0 || state.getInReadDate() < inMaxMessageDate) { state = state .changeCounter(0) @@ -730,12 +885,26 @@ private void markAsReadIfNeeded(Peer peer) { context().getMessagesModule().getPlainReadActor() .send(new CursorReaderActor.MarkRead(peer, inMaxMessageDate)); - notifyActiveDialogsVM(); + needUpdateDialogs = true; - getDialogsRouter().onCounterChanged(peer, 0); + + if (peer.getPeerType() == PeerType.PRIVATE_ENCRYPTED) { + destructor.onMessageReadByMe(peer, inMaxMessageDate); + } context().getNotificationsModule().onOwnRead(peer, inMaxMessageDate); } + + if (peer.getPeerType() == PeerType.PRIVATE_ENCRYPTED) { + encryptedConversationStates.addOrUpdateItem(encryptedConversationStates.getValue(peer.getPeerId()).readAll()); + needUpdateDialogs = true; + } + + if (needUpdateDialogs) { + getDialogsRouter().onCounterChanged(peer, 0); + notifyActiveDialogsVM(); + } + } } @@ -802,12 +971,18 @@ private void notifyActiveDialogsVM() { } groups.add(new DialogGroup(i.getTitle(), i.getKey(), dialogSmalls)); } + + for (Peer p : activeEncryptedDialogGroupStorage.getPeers()) { + int unreadCount = encryptedConversationStates.getValue(p.getPeerId()).getUnreadCount(); + counter += unreadCount; + } + context().getMessagesModule().getDialogGroupsVM().getGroupsValueModel().change(groups); context().getConductor().getGlobalStateVM().onGlobalCounterChanged(counter); } public boolean isValidPeer(Peer peer) { - if (peer.getPeerType() == PeerType.PRIVATE) { + if (peer.getPeerType() == PeerType.PRIVATE || peer.getPeerType() == PeerType.PRIVATE_ENCRYPTED) { return users().getValue(peer.getPeerId()) != null; } else if (peer.getPeerType() == PeerType.GROUP) { return groups().getValue(peer.getPeerId()) != null; @@ -934,6 +1109,68 @@ public Promise onUpdate(Update update) { return Promise.success(null); } + public Promise onEncryptedUpdate(int senderId, long date, ApiEncryptedContent update) { + + if (update instanceof ApiEncryptedMessageContent) { + ApiEncryptedMessageContent content = (ApiEncryptedMessageContent) update; + + Integer timer = content.getTimerMs(); + if (timer == null) { + timer = 0; + } + + Message msg = new Message(content.getRid(), date, date, senderId, + MessageState.UNKNOWN, AbsContent.fromMessage(content.getMessage()), + new ArrayList<>(), 0, timer); + + int destId = senderId; + if (senderId == myUid()) { + destId = content.getReceiverId(); + } + ArrayList messages = new ArrayList<>(); + messages.add(msg); + return onNewMessages(Peer.secret(destId), messages); + } else if (update instanceof ApiEncryptedReceived) { + ApiEncryptedReceived encryptedReceived = (ApiEncryptedReceived) update; + int destId = senderId; + if (senderId == myUid()) { + destId = encryptedReceived.getReceiverId(); + } + return onMessageReceived(Peer.secret(destId), encryptedReceived.getReceiveDate()); + } else if (update instanceof ApiEncryptedRead) { + ApiEncryptedRead encryptedRead = (ApiEncryptedRead) update; + if (senderId == myUid()) { + return onMessageReadByMe(Peer.secret(encryptedRead.getReceiverId()), encryptedRead.getReadDate(), 0); + } else { + return onMessageRead(Peer.secret(senderId), encryptedRead.getReadDate()); + } + } else if (update instanceof ApiEncryptedDeleteContent) { + ApiEncryptedDeleteContent delete = (ApiEncryptedDeleteContent) update; + int destId = senderId; + if (senderId == myUid()) { + destId = delete.getReceiverId(); + } + return onMessageDeleted(Peer.secret(destId), delete.getRid()); + } else if (update instanceof ApiEncryptedEditContent) { + ApiEncryptedEditContent editContent = (ApiEncryptedEditContent) update; + int destId = senderId; + if (senderId == myUid()) { + destId = editContent.getReceiverId(); + } + return onContentUpdate(Peer.secret(destId), editContent.getRid(), + AbsContent.fromMessage(editContent.getMessage())); + } else if (update instanceof ApiEncryptedDeleteAll) { + ApiEncryptedDeleteAll deleteAll = (ApiEncryptedDeleteAll) update; + int destId = senderId; + if (senderId == myUid()) { + destId = deleteAll.getReceiverId(); + } + return onChatClear(Peer.secret(destId)); + } + + return Promise.success(null); + } + @Override public void onReceive(Object message) { if (!activeDialogStorage.isLoaded() && message instanceof RouterMessageOnlyActive) { @@ -963,6 +1200,10 @@ public Promise onAsk(Object message) throws Exception { } if (message instanceof RouterMessageUpdate) { return onUpdate(((RouterMessageUpdate) message).getUpdate()); + } else if (message instanceof RouterEncryptedUpdate) { + RouterEncryptedUpdate encryptedUpdate = (RouterEncryptedUpdate) message; + return onEncryptedUpdate(encryptedUpdate.getSenderId(), encryptedUpdate.getDate(), + encryptedUpdate.getUpdate()); } else if (message instanceof RouterDifferenceStart) { return onDifferenceStart(); } else if (message instanceof RouterDifferenceEnd) { @@ -1016,6 +1257,15 @@ public Promise onAsk(Object message) throws Exception { } else if (message instanceof RouterResetChat) { RouterResetChat resetChat = (RouterResetChat) message; return onChatReset(resetChat.getPeer()); + } else if (message instanceof RouterMessagesSelfDestructed) { + RouterMessagesSelfDestructed selfDestructed = (RouterMessagesSelfDestructed) message; + return onMessageDestructed(selfDestructed.getPeer(), selfDestructed.getRids()); + } else if (message instanceof RouterSecretChatDeleted) { + RouterSecretChatDeleted chatDeleted = (RouterSecretChatDeleted) message; + return onChatDelete(chatDeleted.getPeer()); + } else if (message instanceof RouterSecretChatCleared) { + RouterSecretChatCleared chatCleared = (RouterSecretChatCleared) message; + return onChatClear(chatCleared.getPeer()); } else { return super.onAsk(message); } diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/router/RouterInt.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/router/RouterInt.java index 1bc228aba6..c78cfba9c6 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/router/RouterInt.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/router/RouterInt.java @@ -3,6 +3,7 @@ import java.util.ArrayList; import java.util.List; +import im.actor.core.api.ApiEncryptedContent; import im.actor.core.entity.Group; import im.actor.core.entity.Message; import im.actor.core.entity.Peer; @@ -23,12 +24,17 @@ import im.actor.core.modules.messaging.router.entity.RouterDeletedMessages; import im.actor.core.modules.messaging.router.entity.RouterDifferenceEnd; import im.actor.core.modules.messaging.router.entity.RouterDifferenceStart; +import im.actor.core.modules.messaging.router.entity.RouterEncryptedUpdate; import im.actor.core.modules.messaging.router.entity.RouterMessageUpdate; +import im.actor.core.modules.messaging.router.entity.RouterMessagesSelfDestructed; import im.actor.core.modules.messaging.router.entity.RouterNewMessages; import im.actor.core.modules.messaging.router.entity.RouterOutgoingError; import im.actor.core.modules.messaging.router.entity.RouterOutgoingMessage; +import im.actor.core.modules.messaging.router.entity.RouterOutgoingSent; import im.actor.core.modules.messaging.router.entity.RouterPeersChanged; import im.actor.core.modules.messaging.router.entity.RouterResetChat; +import im.actor.core.modules.messaging.router.entity.RouterSecretChatCleared; +import im.actor.core.modules.messaging.router.entity.RouterSecretChatDeleted; import im.actor.core.network.parser.Update; import im.actor.runtime.actors.ActorInterface; import im.actor.runtime.actors.messages.Void; @@ -40,10 +46,8 @@ public class RouterInt extends ActorInterface implements BusSubscriber { - private final ModuleContext context; - public RouterInt(final ModuleContext context) { - this.context = context; + setDest(system().actorOf("actor/router", () -> new RouterActor(context))); context.getEvents().subscribe(this, PeerChatOpened.EVENT); @@ -64,6 +68,10 @@ public Promise onUpdate(Update update) { return ask(new RouterMessageUpdate(update)); } + public Promise onEncryptedUpdate(int senderId, long date, ApiEncryptedContent update) { + return ask(new RouterEncryptedUpdate(senderId, date, update)); + } + public Promise onDifferenceEnd() { return ask(new RouterDifferenceEnd()); } @@ -96,6 +104,10 @@ public Promise onOutgoingError(Peer peer, long rid) { return ask(new RouterOutgoingError(peer, rid)); } + public Promise onOutgoingSent(Peer peer, long rid, long date) { + return ask(new RouterOutgoingSent(peer, rid, date)); + } + public Promise onContentChanged(Peer peer, long rid, AbsContent content) { return ask(new RouterChangedContent(peer, rid, content)); } @@ -109,6 +121,17 @@ public Promise onMessagesDeleted(Peer peer, List rids) { return ask(new RouterDeletedMessages(peer, rids)); } + public Promise onMessagesDestructed(Peer peer, List rids) { + return ask(new RouterMessagesSelfDestructed(peer, rids)); + } + + public Promise onSecretChatDeleted(Peer peer) { + return ask(new RouterSecretChatDeleted(peer)); + } + + public Promise onSecretChatCleared(Peer peer) { + return ask(new RouterSecretChatCleared(peer)); + } // // History diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/router/entity/RouterEncryptedUpdate.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/router/entity/RouterEncryptedUpdate.java new file mode 100644 index 0000000000..7c0b915c98 --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/router/entity/RouterEncryptedUpdate.java @@ -0,0 +1,30 @@ +package im.actor.core.modules.messaging.router.entity; + +import im.actor.core.api.ApiEncryptedContent; +import im.actor.runtime.actors.ask.AskMessage; +import im.actor.runtime.actors.messages.Void; + +public class RouterEncryptedUpdate implements AskMessage { + + private int senderId; + private long date; + private ApiEncryptedContent update; + + public RouterEncryptedUpdate(int senderId, long date, ApiEncryptedContent update) { + this.senderId = senderId; + this.date = date; + this.update = update; + } + + public int getSenderId() { + return senderId; + } + + public long getDate() { + return date; + } + + public ApiEncryptedContent getUpdate() { + return update; + } +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/router/entity/RouterMessagesSelfDestructed.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/router/entity/RouterMessagesSelfDestructed.java new file mode 100644 index 0000000000..d72a18fb36 --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/router/entity/RouterMessagesSelfDestructed.java @@ -0,0 +1,25 @@ +package im.actor.core.modules.messaging.router.entity; + +import java.util.List; + +import im.actor.core.entity.Peer; +import im.actor.runtime.actors.ask.AskMessage; +import im.actor.runtime.actors.messages.Void; + +public class RouterMessagesSelfDestructed implements AskMessage, RouterMessageOnlyActive { + private final Peer peer; + private final List rids; + + public RouterMessagesSelfDestructed(Peer peer, List rids) { + this.peer = peer; + this.rids = rids; + } + + public Peer getPeer() { + return peer; + } + + public List getRids() { + return rids; + } +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/router/entity/RouterOutgoingSent.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/router/entity/RouterOutgoingSent.java index 3a20809a98..884de2c604 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/router/entity/RouterOutgoingSent.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/router/entity/RouterOutgoingSent.java @@ -1,8 +1,10 @@ package im.actor.core.modules.messaging.router.entity; import im.actor.core.entity.Peer; +import im.actor.runtime.actors.ask.AskMessage; +import im.actor.runtime.actors.messages.Void; -public class RouterOutgoingSent implements RouterMessageOnlyActive { +public class RouterOutgoingSent implements AskMessage, RouterMessageOnlyActive { private Peer peer; private long rid; diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/router/entity/RouterSecretChatCleared.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/router/entity/RouterSecretChatCleared.java new file mode 100644 index 0000000000..5322642c20 --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/router/entity/RouterSecretChatCleared.java @@ -0,0 +1,18 @@ +package im.actor.core.modules.messaging.router.entity; + +import im.actor.core.entity.Peer; +import im.actor.runtime.actors.ask.AskMessage; +import im.actor.runtime.actors.messages.Void; + +public class RouterSecretChatCleared implements AskMessage, RouterMessageOnlyActive { + + private Peer peer; + + public RouterSecretChatCleared(Peer peer) { + this.peer = peer; + } + + public Peer getPeer() { + return peer; + } +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/router/entity/RouterSecretChatDeleted.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/router/entity/RouterSecretChatDeleted.java new file mode 100644 index 0000000000..1fe027aacd --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/router/entity/RouterSecretChatDeleted.java @@ -0,0 +1,18 @@ +package im.actor.core.modules.messaging.router.entity; + +import im.actor.core.entity.Peer; +import im.actor.runtime.actors.ask.AskMessage; +import im.actor.runtime.actors.messages.Void; + +public class RouterSecretChatDeleted implements AskMessage, RouterMessageOnlyActive { + + private Peer peer; + + public RouterSecretChatDeleted(Peer peer) { + this.peer = peer; + } + + public Peer getPeer() { + return peer; + } +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/notifications/NotificationsActor.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/notifications/NotificationsActor.java index 95af715229..4b78e9cc9d 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/notifications/NotificationsActor.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/notifications/NotificationsActor.java @@ -485,7 +485,7 @@ private boolean isNotificationsEnabled(Peer peer, boolean hasMention) { // All group notifications are disabled return false; } - } else if (peer.getPeerType() == PeerType.PRIVATE) { + } else if (peer.getPeerType() == PeerType.PRIVATE || peer.getPeerType() == PeerType.PRIVATE_ENCRYPTED) { // For private conversations only check if peer notifications enabled return context().getSettingsModule().isNotificationsEnabled(peer); diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/presence/PresenceActor.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/presence/PresenceActor.java index e2998d9ebf..b5b259f893 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/presence/PresenceActor.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/presence/PresenceActor.java @@ -331,9 +331,9 @@ public void onBusEvent(Event event) { if (event instanceof NewSessionCreated) { self().send(new SessionCreated()); } else if (event instanceof PeerChatOpened) { - self().send(new Subscribe(((PeerChatOpened) event).getPeer())); + self().send(new Subscribe(((PeerChatOpened) event).getPeer().toUnencryptedCompat())); } else if (event instanceof PeerInfoOpened) { - self().send(new Subscribe(((PeerInfoOpened) event).getPeer())); + self().send(new Subscribe(((PeerInfoOpened) event).getPeer().toUnencryptedCompat())); } else if (event instanceof UserVisible) { self().send(new Subscribe(Peer.user(((UserVisible) event).getUid()))); } diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/profile/avatar/GroupAvatarChangeActor.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/profile/avatar/GroupAvatarChangeActor.java index df6fbbc424..d7b8ae8093 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/profile/avatar/GroupAvatarChangeActor.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/profile/avatar/GroupAvatarChangeActor.java @@ -51,7 +51,7 @@ public void changeAvatar(int gid, String descriptor) { tasksMap.put(rid, gid); context().getGroupsModule().getAvatarVM(gid).getUploadState().change(new AvatarUploadState(descriptor, true)); - context().getFilesModule().requestUpload(rid, descriptor, "avatar.jpg", self()); + context().getFilesModule().requestUpload(rid, descriptor, "avatar.jpg", false, self()); } public void uploadCompleted(final long rid, FileReference fileReference) { diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/profile/avatar/OwnAvatarChangeActor.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/profile/avatar/OwnAvatarChangeActor.java index d5eb441c52..7e1595e382 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/profile/avatar/OwnAvatarChangeActor.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/profile/avatar/OwnAvatarChangeActor.java @@ -38,7 +38,7 @@ public void changeAvatar(String descriptor) { context().getProfileModule().getOwnAvatarVM().getUploadState().change(new AvatarUploadState(descriptor, true)); - context().getFilesModule().requestUpload(currentChangeTask, descriptor, "avatar.jpg", self()); + context().getFilesModule().requestUpload(currentChangeTask, descriptor, "avatar.jpg", false, self()); } public void uploadCompleted(final long rid, FileReference fileReference) { diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/sequence/processor/UpdateProcessor.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/sequence/processor/UpdateProcessor.java index 3c95412bc3..f2771a4722 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/sequence/processor/UpdateProcessor.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/sequence/processor/UpdateProcessor.java @@ -7,19 +7,16 @@ import java.util.ArrayList; import java.util.List; -import im.actor.core.api.ApiGroup; -import im.actor.core.api.ApiUser; import im.actor.core.api.updates.UpdateMessage; import im.actor.core.api.updates.UpdateMessageRead; import im.actor.core.api.updates.UpdateMessageReadByMe; import im.actor.core.api.updates.UpdateMessageReceived; -import im.actor.core.entity.Group; import im.actor.core.entity.Peer; import im.actor.core.modules.AbsModule; import im.actor.core.modules.ModuleContext; import im.actor.core.modules.calls.CallsProcessor; import im.actor.core.modules.contacts.ContactsProcessor; -import im.actor.core.modules.encryption.EncryptedProcessor; +import im.actor.core.modules.encryption.EncryptionProcessor; import im.actor.core.modules.eventbus.EventBusProcessor; import im.actor.core.modules.groups.GroupsProcessor; import im.actor.core.modules.presence.PresenceProcessor; @@ -33,13 +30,9 @@ import im.actor.core.modules.users.UsersProcessor; import im.actor.core.network.parser.Update; import im.actor.runtime.actors.messages.Void; -import im.actor.runtime.function.Function; -import im.actor.runtime.function.Predicate; import im.actor.runtime.function.Supplier; -import im.actor.runtime.function.Tuple2; import im.actor.runtime.promise.Promise; import im.actor.runtime.promise.Promises; -import im.actor.runtime.promise.PromisesArray; public class UpdateProcessor extends AbsModule { @@ -64,7 +57,7 @@ public UpdateProcessor(ModuleContext context) { new UsersProcessor(context), new GroupsProcessor(context), new ContactsProcessor(context), - new EncryptedProcessor(context), + new EncryptionProcessor(context), new StickersProcessor(context), new SettingsProcessor(context), new RawProcessor(context) @@ -148,7 +141,6 @@ public Promise applyDifferenceUpdate(List updates) { pending.add(() -> messagesProcessor.onDifferenceEnd()); - return Promises.traverse(pending) - .map(v -> null); + return Promises.traverse(pending); } } diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/sequence/processor/UpdateValidator.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/sequence/processor/UpdateValidator.java index 21f55884fd..858ec3e252 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/sequence/processor/UpdateValidator.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/sequence/processor/UpdateValidator.java @@ -11,6 +11,7 @@ import im.actor.core.api.updates.UpdateContactRegistered; import im.actor.core.api.updates.UpdateContactsAdded; import im.actor.core.api.updates.UpdateContactsRemoved; +import im.actor.core.api.updates.UpdateEncryptedPackage; import im.actor.core.api.updates.UpdateGroupExtChanged; import im.actor.core.api.updates.UpdateGroupFullExtChanged; import im.actor.core.api.updates.UpdateGroupMemberAdminChanged; @@ -104,7 +105,11 @@ public boolean isCausesInvalidation(Update update) { } else if (update instanceof UpdateGroupExtChanged) { UpdateGroupExtChanged extChanged = (UpdateGroupExtChanged) update; groups.add(extChanged.getGroupId()); + } else if (update instanceof UpdateEncryptedPackage) { + UpdateEncryptedPackage updateEncryptedPackage = (UpdateEncryptedPackage) update; + users.add(updateEncryptedPackage.getSenderId()); } + if (!hasUsers(users)) { return true; } diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/settings/SettingsModule.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/settings/SettingsModule.java index 6dfd60ec0d..be0286bc77 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/settings/SettingsModule.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/settings/SettingsModule.java @@ -408,7 +408,7 @@ public void notifySettingsChanged() { } private String getChatKey(Peer peer) { - if (peer.getPeerType() == PeerType.PRIVATE) { + if (peer.getPeerType() == PeerType.PRIVATE || peer.getPeerType() == PeerType.PRIVATE_ENCRYPTED) { return "PRIVATE_" + peer.getPeerId(); } else if (peer.getPeerType() == PeerType.GROUP) { return "GROUP_" + peer.getPeerId(); diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/typing/TypingActor.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/typing/TypingActor.java index d91d615bc5..d28f3fd24c 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/typing/TypingActor.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/typing/TypingActor.java @@ -26,8 +26,10 @@ public static ActorRef get(final ModuleContext messenger) { private static final int TYPING_TEXT_TIMEOUT = 7000; - private HashMap typingsCancellables = new HashMap<>(); private HashSet typings = new HashSet<>(); + private HashMap typingsCancellables = new HashMap<>(); + private HashSet secretTypings = new HashSet<>(); + private HashMap secretTypingsCancellables = new HashMap<>(); private HashMap> groupTypings = new HashMap<>(); private HashMap> groupCancellables = new HashMap<>(); @@ -71,6 +73,42 @@ private void stopPrivateTyping(int uid) { } } + @Verified + private void privateSecretTyping(int uid, ApiTypingType type) { + // Support only text typings + if (type != ApiTypingType.TEXT) { + return; + } + + if (getUser(uid) == null) { + return; + } + + if (!secretTypings.contains(uid)) { + secretTypings.add(uid); + + context().getTypingModule().getSecretTyping(uid).getTyping().change(true); + } + + if (secretTypingsCancellables.containsKey(uid)) { + secretTypingsCancellables.remove(uid).cancel(); + } + secretTypingsCancellables.put(uid, schedule(new StopSecretTyping(uid), TYPING_TEXT_TIMEOUT)); + } + + @Verified + private void stopPrivateSecretTyping(int uid) { + if (secretTypings.contains(uid)) { + secretTypings.remove(uid); + + if (secretTypingsCancellables.containsKey(uid)) { + secretTypingsCancellables.remove(uid).cancel(); + } + + context().getTypingModule().getSecretTyping(uid).getTyping().change(false); + } + } + @Verified private void groupTyping(int gid, int uid, ApiTypingType type) { // Support only text typings @@ -151,12 +189,18 @@ public void onReceive(Object message) { if (message instanceof PrivateTyping) { PrivateTyping typing = (PrivateTyping) message; privateTyping(typing.getUid(), typing.getType()); + } else if (message instanceof PrivateSecretTyping) { + PrivateSecretTyping typing = (PrivateSecretTyping) message; + privateSecretTyping(typing.getUid(), typing.getType()); } else if (message instanceof GroupTyping) { GroupTyping typing = (GroupTyping) message; groupTyping(typing.getGid(), typing.getUid(), typing.getType()); } else if (message instanceof StopTyping) { StopTyping typing = (StopTyping) message; stopPrivateTyping(typing.getUid()); + } else if (message instanceof StopSecretTyping) { + StopSecretTyping secretTyping = (StopSecretTyping) message; + stopPrivateSecretTyping(secretTyping.getUid()); } else if (message instanceof StopGroupTyping) { StopGroupTyping typing = (StopGroupTyping) message; stopGroupTyping(typing.getGid(), typing.getUid()); @@ -194,6 +238,36 @@ public int hashCode() { } } + public static class StopSecretTyping { + + private int uid; + + public StopSecretTyping(int uid) { + this.uid = uid; + } + + public int getUid() { + return uid; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + StopSecretTyping that = (StopSecretTyping) o; + + if (uid != that.uid) return false; + + return true; + } + + @Override + public int hashCode() { + return uid; + } + } + public static class StopGroupTyping { private int gid; private int uid; @@ -233,6 +307,7 @@ public int hashCode() { } public static class PrivateTyping { + private int uid; private ApiTypingType type; @@ -270,6 +345,45 @@ public int hashCode() { } } + public static class PrivateSecretTyping { + + private int uid; + private ApiTypingType type; + + public PrivateSecretTyping(int uid, ApiTypingType type) { + this.uid = uid; + this.type = type; + } + + public int getUid() { + return uid; + } + + public ApiTypingType getType() { + return type; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + PrivateTyping that = (PrivateTyping) o; + + if (type != that.type) return false; + if (uid != that.uid) return false; + + return true; + } + + @Override + public int hashCode() { + int result = uid; + result = 31 * result + type.getValue(); + return result; + } + } + public static class GroupTyping { private int gid; private int uid; diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/typing/TypingModule.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/typing/TypingModule.java index dabfdde2c8..1de205d992 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/typing/TypingModule.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/typing/TypingModule.java @@ -19,8 +19,9 @@ public class TypingModule extends AbsModule { private ActorRef ownTypingActor; private ActorRef typingActor; - private HashMap uids = new HashMap<>(); - private HashMap groups = new HashMap<>(); + private final HashMap uids = new HashMap<>(); + private final HashMap sec_uids = new HashMap<>(); + private final HashMap groups = new HashMap<>(); public TypingModule(final ModuleContext context) { super(context); @@ -47,6 +48,15 @@ public UserTypingVM getTyping(int uid) { } } + public UserTypingVM getSecretTyping(int uid) { + synchronized (sec_uids) { + if (!sec_uids.containsKey(uid)) { + sec_uids.put(uid, new UserTypingVM(uid)); + } + return sec_uids.get(uid); + } + } + public void onTyping(Peer peer) { ownTypingActor.send(new OwnTypingActor.Typing(peer)); } diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/network/mtp/actors/ManagerActor.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/network/mtp/actors/ManagerActor.java index d44a33984f..763fbd6ee5 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/network/mtp/actors/ManagerActor.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/network/mtp/actors/ManagerActor.java @@ -85,7 +85,7 @@ public ManagerActor(MTProto mtProto) { if (this.authKey != null) { this.authProtoKey = new ActorProtoKey(this.authKey); this.serverUSDecryptor = new CBCHmacBox( - Crypto.createAES128(this.authProtoKey.getServerKey()), + Crypto.createAES256(this.authProtoKey.getServerKey()), Crypto.createSHA256(), this.authProtoKey.getServerMacKey()); this.serverRUDecryptor = new CBCHmacBox( @@ -93,7 +93,7 @@ public ManagerActor(MTProto mtProto) { new Streebog256(), this.authProtoKey.getServerMacRussianKey()); this.clientUSEncryptor = new CBCHmacBox( - Crypto.createAES128(this.authProtoKey.getClientKey()), + Crypto.createAES256(this.authProtoKey.getClientKey()), Crypto.createSHA256(), this.authProtoKey.getClientMacKey()); this.clientRUEncryptor = new CBCHmacBox( diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/util/JavaUtil.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/util/JavaUtil.java index 670e0b0a00..ba26766bae 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/util/JavaUtil.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/util/JavaUtil.java @@ -82,4 +82,20 @@ public static long[] unbox(List list) { return res; } + /** + * Compare Longs + * + * @param a first value + * @param b second value + * @return result + */ + public static int compare(long a, long b) { + if (a == b) { + return 0; + } else if (a < b) { + return -1; + } else { + return 1; + } + } } diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/viewmodel/ConversationVM.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/viewmodel/ConversationVM.java index 1c91885870..4f633d552a 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/viewmodel/ConversationVM.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/viewmodel/ConversationVM.java @@ -8,12 +8,7 @@ public class ConversationVM extends BaseValueModel { - public static ValueModelCreator CREATOR = new ValueModelCreator() { - @Override - public ConversationVM create(ConversationState baseValue) { - return new ConversationVM(baseValue); - } - }; + public static ValueModelCreator CREATOR = baseValue -> new ConversationVM(baseValue); private BooleanValueModel isLoaded; private BooleanValueModel isEmpty; diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/viewmodel/EncryptedConversationVM.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/viewmodel/EncryptedConversationVM.java new file mode 100644 index 0000000000..cf1b9b864d --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/viewmodel/EncryptedConversationVM.java @@ -0,0 +1,41 @@ +package im.actor.core.viewmodel; + +import im.actor.core.entity.EncryptedConversationState; +import im.actor.core.viewmodel.generics.IntValueModel; +import im.actor.runtime.mvvm.BaseValueModel; +import im.actor.runtime.mvvm.ValueModelCreator; + +public class EncryptedConversationVM extends BaseValueModel { + + public static ValueModelCreator CREATOR = + baseValue -> new EncryptedConversationVM(baseValue); + + private int uid; + private IntValueModel timer; + private IntValueModel counter; + + public EncryptedConversationVM(EncryptedConversationState rawObj) { + super(rawObj); + uid = rawObj.getUid(); + timer = new IntValueModel("encrypted_" + uid + ".timer", rawObj.getTimer()); + counter = new IntValueModel("encrypted_" + uid + ".counter", rawObj.getUnreadCount()); + } + + public int getUid() { + return uid; + } + + public IntValueModel getTimer() { + return timer; + } + + public IntValueModel getCounter() { + return counter; + } + + @Override + protected void updateValues(EncryptedConversationState rawObj) { + timer.change(rawObj.getTimer()); + counter.change(rawObj.getUnreadCount()); + } +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/resources/AppText.json b/actor-sdk/sdk-core/core/core-shared/src/main/resources/AppText.json index 53778a8d92..718cac65a6 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/resources/AppText.json +++ b/actor-sdk/sdk-core/core/core-shared/src/main/resources/AppText.json @@ -1,206 +1,212 @@ { - "app": { - "name": "Actor" - }, - - "language": { - - "code": "En", - - "format": { - "time": { - "yesterday":{ - "short": "Yest", - "full": "Yesterday" - }, - "now": "Now", - "minutes": { - "short": "{minutes} min", - "full": "{minutes} minutes" - }, - "hours": { - "short": "{hours} hrs", - "full": "{hours} hours" - } - } - }, - - "file_size": { - "bytes": "{bytes} B", - "kbytes": "{kbytes} KB", - "mbytes": "{mbytes} MB", - "gbytes": "{gbytes} GB" - }, - - "referencing": { - "you": "You", - "thee": "You" - }, - - "sequence": { - "or": ", ", - "and": " and " - } - }, - - "groups": { - "members": { - "one": "{count} member", - "other": "{count} members" - } - }, - - "presence": { - "online": "online", - "offline": "offline", - "now": "last seen just now", - "today": "last seen today at {time}", - "yesterday": "last seen yesterday at {time}", - "at_day": "last seen {date}", - "at_day_time": "last seen {date} at {time}", - "members": "{count} online" - }, - - "typing": { - "simple": "typing...", - "user": "{user} is typing...", - "group": { - "sequenced": "{users} are typing...", - "many": "{count} people are typing..." - } - }, - - "content": { - - "unknown": "Unsupported content", - "document": "Document", - "photo": "Photo", - "video": "Video", - "audio": "Audio", - "contact": "Contact", - "location": "Location", - "sticker": "Sticker", - - "service": { - "registered": { - "full": "{name} joined {app_name}", - "compact": "Joined {app_name}" - }, - "groups": { - "created": "{name} created the group", - "invited": "{name} invited {name_added}", - "joined": "{name} joined group", - "kicked": "{name} kicked {name_kicked}", - "left": "{name} left group", - "title_changed": { - "full": "{name} changed the group name to \"{title}\"", - "compact": "{name} changed the group name" - }, - "topic_changed": { - "full": "{name} changed the group topic to \"{topic}\"", - "compact": "{name} changed the group topic" - }, - "about_changed": { - "full": "{name} changed the group about to \"{about}\"", - "compact": "{name} changed the group about" - }, - "avatar_changed": "{name} changed the group photo", - "avatar_removed": "{name} removed the group photo" - }, - "channels": { - "created": "Channel created", - "invited": "{name} invited {name_added}", - "joined": "{name} joined channel", - "kicked": "{name} kicked {name_kicked}", - "left": "{name} left channel", - "title_changed": { - "full": "{name} changed the channel name to \"{title}\"", - "compact": "{name} changed the channel name" - }, - "topic_changed": { - "full": "{name} changed the channel topic to \"{topic}\"", - "compact": "{name} changed the channel topic" - }, - "about_changed": { - "full": "{name} changed the channel about to \"{about}\"", - "compact": "{name} changed the channel about" - }, - "avatar_changed": "{name} changed the channel photo", - "avatar_removed": "{name} removed the channel photo" - }, - "calls": { - "missed": "Missed call", - "ended": "Call ended" - } - } - }, - - "errors": { - "unknown": "Unknown error. Please, try again later.", - "internal": "Internal server error. Please, try again later.", - "phone": { - "empty": "Please, enter your phone number.", - "incorrect": "Entered phone number is invalid. Please, try again." - }, - "code": { - "empty": "The code is invalid. Please try again.", - "incorrect": "Entered code is incorrect. Please, try again.", - "expired": "The code has expired. Please, start authentication again." - }, - "groups": { - "already_joined": "You have already joined the group.", - "unable_to_join": "Unable to join the group." - } - }, - - "months": { - "january": { - "compact": "jan", - "full":"january" - }, - "february": { - "compact": "feb", - "full":"february" - }, - "march": { - "compact": "mar", - "full":"march" - }, - "april": { - "compact": "apr", - "full":"april" - }, - "may": { - "compact": "may", - "full":"may" - }, - "june": { - "compact": "jun", - "full":"june" - }, - "july": { - "compact": "jul", - "full":"july" - }, - "august": { - "compact": "aug", - "full":"august" - }, - "september": { - "compact": "sep", - "full":"september" - }, - "october": { - "compact": "oct", - "full":"october" - }, - "november": { - "compact": "nov", - "full":"november" - }, - "december": { - "compact": "dec", - "full":"december" - } - } + "app": { + "name": "Actor" + }, + "language": { + "code": "En", + "format": { + "time": { + "yesterday": { + "short": "Yest", + "full": "Yesterday" + }, + "now": "Now", + "seconds": { + "short": "{seconds} sec", + "full": { + "one": "{seconds} second", + "other": "{seconds} seconds" + } + }, + "minutes": { + "short": "{minutes} min", + "full": { + "one": "{minutes} minute", + "other": "{minutes} minutes" + } + }, + "hours": { + "short": "{hours} hrs", + "full": { + "one": "{hours} hour", + "other": "{hours} hours" + } + } + } + }, + "file_size": { + "bytes": "{bytes} B", + "kbytes": "{kbytes} KB", + "mbytes": "{mbytes} MB", + "gbytes": "{gbytes} GB" + }, + "referencing": { + "you": "You", + "thee": "You" + }, + "sequence": { + "or": ", ", + "and": " and " + } + }, + "groups": { + "members": { + "one": "{count} member", + "other": "{count} members" + } + }, + "presence": { + "online": "online", + "offline": "offline", + "now": "last seen just now", + "today": "last seen today at {time}", + "yesterday": "last seen yesterday at {time}", + "at_day": "last seen {date}", + "at_day_time": "last seen {date} at {time}", + "members": "{count} online" + }, + "typing": { + "simple": "typing...", + "user": "{user} is typing...", + "group": { + "sequenced": "{users} are typing...", + "many": "{count} people are typing..." + } + }, + "content": { + "unknown": "Unsupported content", + "document": "Document", + "photo": "Photo", + "video": "Video", + "audio": "Audio", + "contact": "Contact", + "location": "Location", + "sticker": "Sticker", + "service": { + "registered": { + "full": "{name} joined {app_name}", + "compact": "Joined {app_name}" + }, + "groups": { + "created": "{name} created the group", + "invited": "{name} invited {name_added}", + "joined": "{name} joined group", + "kicked": "{name} kicked {name_kicked}", + "left": "{name} left group", + "title_changed": { + "full": "{name} changed the group name to \"{title}\"", + "compact": "{name} changed the group name" + }, + "topic_changed": { + "full": "{name} changed the group topic to \"{topic}\"", + "compact": "{name} changed the group topic" + }, + "about_changed": { + "full": "{name} changed the group about to \"{about}\"", + "compact": "{name} changed the group about" + }, + "avatar_changed": "{name} changed the group photo", + "avatar_removed": "{name} removed the group photo" + }, + "channels": { + "created": "Channel created", + "invited": "{name} invited {name_added}", + "joined": "{name} joined channel", + "kicked": "{name} kicked {name_kicked}", + "left": "{name} left channel", + "title_changed": { + "full": "{name} changed the channel name to \"{title}\"", + "compact": "{name} changed the channel name" + }, + "topic_changed": { + "full": "{name} changed the channel topic to \"{topic}\"", + "compact": "{name} changed the channel topic" + }, + "about_changed": { + "full": "{name} changed the channel about to \"{about}\"", + "compact": "{name} changed the channel about" + }, + "avatar_changed": "{name} changed the channel photo", + "avatar_removed": "{name} removed the channel photo" + }, + "calls": { + "missed": "Missed call", + "ended": "Call ended" + }, + "encrypted": { + "timer_changed": { + "full": "{name} set self-destruct timer to {timer}", + "compact": "{name} set self-destruct timer" + }, + "timer_disabled": "{name} disabled self-destruct timer" + } + } + }, + "errors": { + "unknown": "Unknown error. Please, try again later.", + "internal": "Internal server error. Please, try again later.", + "phone": { + "empty": "Please, enter your phone number.", + "incorrect": "Entered phone number is invalid. Please, try again." + }, + "code": { + "empty": "The code is invalid. Please try again.", + "incorrect": "Entered code is incorrect. Please, try again.", + "expired": "The code has expired. Please, start authentication again." + }, + "groups": { + "already_joined": "You have already joined the group.", + "unable_to_join": "Unable to join the group." + } + }, + "months": { + "january": { + "compact": "jan", + "full": "january" + }, + "february": { + "compact": "feb", + "full": "february" + }, + "march": { + "compact": "mar", + "full": "march" + }, + "april": { + "compact": "apr", + "full": "april" + }, + "may": { + "compact": "may", + "full": "may" + }, + "june": { + "compact": "jun", + "full": "june" + }, + "july": { + "compact": "jul", + "full": "july" + }, + "august": { + "compact": "aug", + "full": "august" + }, + "september": { + "compact": "sep", + "full": "september" + }, + "october": { + "compact": "oct", + "full": "october" + }, + "november": { + "compact": "nov", + "full": "november" + }, + "december": { + "compact": "dec", + "full": "december" + } + } } \ No newline at end of file diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/resources/AppText_Ar.json b/actor-sdk/sdk-core/core/core-shared/src/main/resources/AppText_Ar.json index 308c7b5142..858c9b4c2d 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/resources/AppText_Ar.json +++ b/actor-sdk/sdk-core/core/core-shared/src/main/resources/AppText_Ar.json @@ -1,157 +1,152 @@ { - "app": { - "name": "Actor" - }, - - "language": { - "code": "Ar", - - "format": { - "time": { - "yesterday": "امس", - "now": "الان", - "minutes": "{minutes} دقايق", - "hours": "{hours} ساعات" - } - }, - - "file_size": { - "bytes": "{bytes} بايت", - "kbytes": "{kbytes} كيلو بايت", - "mbytes": "{mbytes} ميجا بايت", - "gbytes": "{gbytes} جيجا بايت" - }, - - "referencing": { - "you": "انت", - "thee": "انت" - }, - - "sequence": { - "or": ",", - "and": "," - } - }, - - "groups": { - "members": "{count} اعضاء" - }, - - "presence": { - "online": "متصل", - "offline": "غير متصل", - "now": "اخر ظهور من", - "today": "اخر ظهور اليوم {time}", - "yesterday": "اخر ظهور بالامس {time}", - "at_day": "اخر ظهور {date}", - "at_day_time": "اخر ظهور {date} على {time}", - "members": "{count} متصلين" - }, - - "typing": { - "simple": "كتابة...", - "user": "{user} يكتب...", - "group": { - "sequenced": "{users} يكتبون...", - "many": "{count} اشخاص يكتبون..." - } - }, - - "content": { - - "unknown": "غير مدعوم", - "document": "مستند", - "photo": "صورة", - "video": "فيديو", - "audio": "Audio", - "contact": "Contact", - "location": "Location", - "sticker": "Sticker", - - "service": { - "registered": { - "full": "{name} انضم لشبكة اكتور {app_name}", - "compact": "انضم لأكتور {app_name}" - }, - "groups": { - "created": "{name} انشأ المجموعة", - "invited": "{name} دعى {name_added}", - "joined": "{name} انصم للمجموعة", - "kicked": "{name} ازال {name_kicked}", - "left": "{name} غادر", - "title_changed": { - "full": "{name} غير الاسم الى \"{title}\"", - "compact": "{name} غير اسم المجموعة الى" - }, - "topic_changed": { - "full": "{name} changed the group topic to \"{topic}\"", - "compact": "{name} changed the group topic" - }, - "about_changed": { - "full": "{name} changed the group about to \"{about}\"", - "compact": "{name} changed the group about" - }, - "avatar_changed": "{name} غير صورة المجموعة", - "avatar_removed": "{name} حذف صورة المجموعة" - }, - "channels": { - "created": " انشأ المجموعة", - "invited": "{name} دعى {name_added}", - "joined": "{name} انصم للمجموعة", - "kicked": "{name} ازال {name_kicked}", - "left": "{name} غادر", - "title_changed": { - "full": "{name} غير الاسم الى \"{title}\"", - "compact": "{name} غير اسم المجموعة الى" - }, - "topic_changed": { - "full": "{name} changed the group topic to \"{topic}\"", - "compact": "{name} changed the group topic" - }, - "about_changed": { - "full": "{name} changed the group about to \"{about}\"", - "compact": "{name} changed the group about" - }, - "avatar_changed": "{name} غير صورة المجموعة", - "avatar_removed": "{name} حذف صورة المجموعة" - }, - "calls": { - "missed": "Missed call", - "ended": "Call ended" - } - } - }, - - "errors": { - "unknown": "Unknown error. Please, try again later.", - "internal": "Internal server error. Please, try again later.", - "phone": { - "empty": "Please, enter your phone number.", - "incorrect": "Entered phone number is invalid. Please, try again." - }, - "code": { - "empty": "The code is invalid. Please try again.", - "incorrect": "Entered code is incorrect. Please, try again.", - "expired": "The code has expired. Please, start authentication again." - }, - "groups": { - "already_joined": "You have already joined the group.", - "unable_to_join": "Unable to join the group." - } - }, - - "months": { - "january": "يناير", - "february": "فبراير", - "march": "مارس", - "april": "ابريل", - "may": "مايو", - "june": "يونيو", - "july": "يوليو", - "august": "اغسطس", - "september": "سمبتمر", - "october": "اكتوبر", - "november": "نوفمبر", - "december": "ديسمبر" - } + "app": { + "name": "Actor" + }, + "language": { + "code": "Ar", + "format": { + "time": { + "yesterday": "امس", + "now": "الان", + "seconds": "{seconds} seconds", + "minutes": "{minutes} دقايق", + "hours": "{hours} ساعات" + } + }, + "file_size": { + "bytes": "{bytes} بايت", + "kbytes": "{kbytes} كيلو بايت", + "mbytes": "{mbytes} ميجا بايت", + "gbytes": "{gbytes} جيجا بايت" + }, + "referencing": { + "you": "انت", + "thee": "انت" + }, + "sequence": { + "or": ",", + "and": "," + } + }, + "groups": { + "members": "{count} اعضاء" + }, + "presence": { + "online": "متصل", + "offline": "غير متصل", + "now": "اخر ظهور من", + "today": "اخر ظهور اليوم {time}", + "yesterday": "اخر ظهور بالامس {time}", + "at_day": "اخر ظهور {date}", + "at_day_time": "اخر ظهور {date} على {time}", + "members": "{count} متصلين" + }, + "typing": { + "simple": "كتابة...", + "user": "{user} يكتب...", + "group": { + "sequenced": "{users} يكتبون...", + "many": "{count} اشخاص يكتبون..." + } + }, + "content": { + "unknown": "غير مدعوم", + "document": "مستند", + "photo": "صورة", + "video": "فيديو", + "audio": "Audio", + "contact": "Contact", + "location": "Location", + "sticker": "Sticker", + "service": { + "registered": { + "full": "{name} انضم لشبكة اكتور {app_name}", + "compact": "انضم لأكتور {app_name}" + }, + "groups": { + "created": "{name} انشأ المجموعة", + "invited": "{name} دعى {name_added}", + "joined": "{name} انصم للمجموعة", + "kicked": "{name} ازال {name_kicked}", + "left": "{name} غادر", + "title_changed": { + "full": "{name} غير الاسم الى \"{title}\"", + "compact": "{name} غير اسم المجموعة الى" + }, + "topic_changed": { + "full": "{name} changed the group topic to \"{topic}\"", + "compact": "{name} changed the group topic" + }, + "about_changed": { + "full": "{name} changed the group about to \"{about}\"", + "compact": "{name} changed the group about" + }, + "avatar_changed": "{name} غير صورة المجموعة", + "avatar_removed": "{name} حذف صورة المجموعة" + }, + "channels": { + "created": " انشأ المجموعة", + "invited": "{name} دعى {name_added}", + "joined": "{name} انصم للمجموعة", + "kicked": "{name} ازال {name_kicked}", + "left": "{name} غادر", + "title_changed": { + "full": "{name} غير الاسم الى \"{title}\"", + "compact": "{name} غير اسم المجموعة الى" + }, + "topic_changed": { + "full": "{name} changed the group topic to \"{topic}\"", + "compact": "{name} changed the group topic" + }, + "about_changed": { + "full": "{name} changed the group about to \"{about}\"", + "compact": "{name} changed the group about" + }, + "avatar_changed": "{name} غير صورة المجموعة", + "avatar_removed": "{name} حذف صورة المجموعة" + }, + "calls": { + "missed": "Missed call", + "ended": "Call ended" + }, + "encrypted": { + "timer_changed": { + "full": "{name} set self-destruct timer to {timer}", + "compact": "{name} set self-destruct timer" + }, + "timer_disabled": "{name} disabled self-destruct timer" + } + } + }, + "errors": { + "unknown": "Unknown error. Please, try again later.", + "internal": "Internal server error. Please, try again later.", + "phone": { + "empty": "Please, enter your phone number.", + "incorrect": "Entered phone number is invalid. Please, try again." + }, + "code": { + "empty": "The code is invalid. Please try again.", + "incorrect": "Entered code is incorrect. Please, try again.", + "expired": "The code has expired. Please, start authentication again." + }, + "groups": { + "already_joined": "You have already joined the group.", + "unable_to_join": "Unable to join the group." + } + }, + "months": { + "january": "يناير", + "february": "فبراير", + "march": "مارس", + "april": "ابريل", + "may": "مايو", + "june": "يونيو", + "july": "يوليو", + "august": "اغسطس", + "september": "سمبتمر", + "october": "اكتوبر", + "november": "نوفمبر", + "december": "ديسمبر" + } } \ No newline at end of file diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/resources/AppText_Es.json b/actor-sdk/sdk-core/core/core-shared/src/main/resources/AppText_Es.json index 7cdcd77f9d..499c0df300 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/resources/AppText_Es.json +++ b/actor-sdk/sdk-core/core/core-shared/src/main/resources/AppText_Es.json @@ -1,199 +1,197 @@ { - "app": { - "name": "Actor" - }, - - "language": { - "code": "Es", - - "format": { - "time": { - "yesterday": "Ayer", - "now": "Ahora", - "minutes": { - "short": "{minutes} min", - "full": "{minutes} minutes" - }, - "hours": { - "short": "{hours} hrs", - "full": "{hours} hours" - } - } - }, - - "file_size": { - "bytes": "{bytes} B", - "kbytes": "{kbytes} KB", - "mbytes": "{mbytes} MB", - "gbytes": "{gbytes} GB" - }, - - "referencing": { - "you": "Tu", - "thee": "Tu" - }, - - "sequence": { - "or": ", ", - "and": " and " - } - }, - - "groups": { - "members": "{count} miembros" - }, - - "presence": { - "online": "En línea", - "offline": "Desconectado", - "now": "visto segundos atrás", - "today": "últ. vez {time}", - "yesterday": "últ. vez ayer {time}", - "at_day": "ult. vez el {date}", - "at_day_time": "ult. vez el {date} a las {time}", - "members": "{count} en línea" - }, - - "typing": { - "simple": "escribiendo...", - "user": "{user} está escribiendo...", - "group": { - "sequenced": "{users} estan escribiendo...", - "many": "{count} personas estan escribiendo..." - } - }, - - "content": { - - "unknown": "Contenido no compatible", - "document": "Documentos", - "photo": "Foto", - "video": "Vídeo", - "audio": "Audio", - "contact": "Contacto", - "location": "Localización", - "sticker": "Sticker", - - "service": { - "registered": { - "full": "{name} unido a la red {app_name}", - "compact": "Entro en {app_name}" - }, - "groups": { - "created": "{name} сreado el grupo", - "invited": "{name} invitó a {name_added}", - "joined": "{name} se unió al grupo", - "kicked": "{name} expulsaste a {name_kicked}", - "left": "{name} elimino el grupo", - "title_changed": { - "full": "{name} cambio el nombre del grupo a \"{title}\"", - "compact": "{name} a modificado el nombre del grupo" - }, - "topic_changed": { - "full": "{name} changed the group topic to \"{topic}\"", - "compact": "{name} changed the group topic" - }, - "about_changed": { - "full": "{name} changed the group about to \"{about}\"", - "compact": "{name} changed the group about" - }, - "avatar_changed": "{name} modificada la foto de grupo", - "avatar_removed": "{name} eliminada la foto de grupo" - }, - "channels": { - "created": "Creado el canal", - "invited": "{name} invitó a {name_added}", - "joined": "{name} se unió al canal", - "kicked": "{name} expulsaste a {name_kicked}", - "left": "{name} elimino el canal", - "title_changed": { - "full": "{name} cambio el nombre del canal a \"{title}\"", - "compact": "{name} a modificado el nombre del canal" - }, - "topic_changed": { - "full": "{name} changed the canal topic to \"{topic}\"", - "compact": "{name} changed the canal topic" - }, - "about_changed": { - "full": "{name} changed the canal about to \"{about}\"", - "compact": "{name} changed the canal about" - }, - "avatar_changed": "{name} modificada la foto de canal", - "avatar_removed": "{name} eliminada la foto de canal" - }, - "calls": { - "missed": "Llamada perdida", - "ended": "Llamada finalizada" - } - } - }, - - "errors": { - "unknown": "Unknown error. Please, try again later.", - "internal": "Error Interno del Servidor. Por favor, inténtelo de nuevo más tarde.", - "phone": { - "empty": "Por favor, introduce tu número de teléfono.", - "incorrect": "Número de teléfono introducido no es válido. Por favor, vuelve a intentarlo." - }, - "code": { - "empty": "Código no válido. Por favor, vuelve a intentarlo.", - "incorrect": "El código introducido es incorrecta. Por favor, vuelve a intentarlo.", - "expired": "Código expirado. Por favor, inicia la autenticación de nuevo." - }, - "groups": { - "already_joined": "Ya eres miembro del grupo.", - "unable_to_join": "No puede unirse al grupo." - } - }, - - "months": { - "january": { - "compact": "ene", - "full":"enero" - }, - "february": { - "compact": "feb", - "full":"febrero" - }, - "march": { - "compact": "mar", - "full":"marzo" - }, - "april": { - "compact": "apr", - "full":"abril" - }, - "may": { - "compact": "may", - "full":"mayo" - }, - "june": { - "compact": "jun", - "full":"junio" - }, - "july": { - "compact": "jul", - "full":"july" - }, - "august": { - "compact": "ago", - "full":"julio" - }, - "september": { - "compact": "sep", - "full":"septembre" - }, - "october": { - "compact": "oct", - "full":"octubre" - }, - "november": { - "compact": "nov", - "full":"noviembre" - }, - "december": { - "compact": "dic", - "full":"diciembre" - } - } + "app": { + "name": "Actor" + }, + "language": { + "code": "Es", + "format": { + "time": { + "yesterday": "Ayer", + "now": "Ahora", + "seconds": { + "short": "{minutes} sec", + "full": "{minutes} seconds" + }, + "minutes": { + "short": "{minutes} min", + "full": "{minutes} minutes" + }, + "hours": { + "short": "{hours} hrs", + "full": "{hours} hours" + } + } + }, + "file_size": { + "bytes": "{bytes} B", + "kbytes": "{kbytes} KB", + "mbytes": "{mbytes} MB", + "gbytes": "{gbytes} GB" + }, + "referencing": { + "you": "Tu", + "thee": "Tu" + }, + "sequence": { + "or": ", ", + "and": " and " + } + }, + "groups": { + "members": "{count} miembros" + }, + "presence": { + "online": "En línea", + "offline": "Desconectado", + "now": "visto segundos atrás", + "today": "últ. vez {time}", + "yesterday": "últ. vez ayer {time}", + "at_day": "ult. vez el {date}", + "at_day_time": "ult. vez el {date} a las {time}", + "members": "{count} en línea" + }, + "typing": { + "simple": "escribiendo...", + "user": "{user} está escribiendo...", + "group": { + "sequenced": "{users} estan escribiendo...", + "many": "{count} personas estan escribiendo..." + } + }, + "content": { + "unknown": "Contenido no compatible", + "document": "Documentos", + "photo": "Foto", + "video": "Vídeo", + "audio": "Audio", + "contact": "Contacto", + "location": "Localización", + "sticker": "Sticker", + "service": { + "registered": { + "full": "{name} unido a la red {app_name}", + "compact": "Entro en {app_name}" + }, + "groups": { + "created": "{name} сreado el grupo", + "invited": "{name} invitó a {name_added}", + "joined": "{name} se unió al grupo", + "kicked": "{name} expulsaste a {name_kicked}", + "left": "{name} elimino el grupo", + "title_changed": { + "full": "{name} cambio el nombre del grupo a \"{title}\"", + "compact": "{name} a modificado el nombre del grupo" + }, + "topic_changed": { + "full": "{name} changed the group topic to \"{topic}\"", + "compact": "{name} changed the group topic" + }, + "about_changed": { + "full": "{name} changed the group about to \"{about}\"", + "compact": "{name} changed the group about" + }, + "avatar_changed": "{name} modificada la foto de grupo", + "avatar_removed": "{name} eliminada la foto de grupo" + }, + "channels": { + "created": "Creado el canal", + "invited": "{name} invitó a {name_added}", + "joined": "{name} se unió al canal", + "kicked": "{name} expulsaste a {name_kicked}", + "left": "{name} elimino el canal", + "title_changed": { + "full": "{name} cambio el nombre del canal a \"{title}\"", + "compact": "{name} a modificado el nombre del canal" + }, + "topic_changed": { + "full": "{name} changed the canal topic to \"{topic}\"", + "compact": "{name} changed the canal topic" + }, + "about_changed": { + "full": "{name} changed the canal about to \"{about}\"", + "compact": "{name} changed the canal about" + }, + "avatar_changed": "{name} modificada la foto de canal", + "avatar_removed": "{name} eliminada la foto de canal" + }, + "calls": { + "missed": "Llamada perdida", + "ended": "Llamada finalizada" + }, + "encrypted": { + "timer_changed": { + "full": "{name} set self-destruct timer to {timer}", + "compact": "{name} set self-destruct timer" + }, + "timer_disabled": "{name} disabled self-destruct timer" + } + } + }, + "errors": { + "unknown": "Unknown error. Please, try again later.", + "internal": "Error Interno del Servidor. Por favor, inténtelo de nuevo más tarde.", + "phone": { + "empty": "Por favor, introduce tu número de teléfono.", + "incorrect": "Número de teléfono introducido no es válido. Por favor, vuelve a intentarlo." + }, + "code": { + "empty": "Código no válido. Por favor, vuelve a intentarlo.", + "incorrect": "El código introducido es incorrecta. Por favor, vuelve a intentarlo.", + "expired": "Código expirado. Por favor, inicia la autenticación de nuevo." + }, + "groups": { + "already_joined": "Ya eres miembro del grupo.", + "unable_to_join": "No puede unirse al grupo." + } + }, + "months": { + "january": { + "compact": "ene", + "full": "enero" + }, + "february": { + "compact": "feb", + "full": "febrero" + }, + "march": { + "compact": "mar", + "full": "marzo" + }, + "april": { + "compact": "apr", + "full": "abril" + }, + "may": { + "compact": "may", + "full": "mayo" + }, + "june": { + "compact": "jun", + "full": "junio" + }, + "july": { + "compact": "jul", + "full": "july" + }, + "august": { + "compact": "ago", + "full": "julio" + }, + "september": { + "compact": "sep", + "full": "septembre" + }, + "october": { + "compact": "oct", + "full": "octubre" + }, + "november": { + "compact": "nov", + "full": "noviembre" + }, + "december": { + "compact": "dic", + "full": "diciembre" + } + } } \ No newline at end of file diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/resources/AppText_Fa.json b/actor-sdk/sdk-core/core/core-shared/src/main/resources/AppText_Fa.json index 0d663fece5..fe24ed4620 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/resources/AppText_Fa.json +++ b/actor-sdk/sdk-core/core/core-shared/src/main/resources/AppText_Fa.json @@ -1,157 +1,152 @@ { - "app": { - "name": "Actor" - }, - - "language": { - "code": "Fa", - - "format": { - "time": { - "yesterday": "دیروز", - "now": "الآن", - "minutes": "{minutes} دقیقه", - "hours": "{hours} ساعت" - } - }, - - "file_size": { - "bytes": "{bytes} B", - "kbytes": "{kbytes} KB", - "mbytes": "{mbytes} MB", - "gbytes": "{gbytes} GB" - }, - - "referencing": { - "you": "شما", - "thee": "شما" - }, - - "sequence": { - "or": ",", - "and": "," - } - }, - - "groups": { - "members": "{count} نفر" - }, - - "presence": { - "online": "برخط", - "offline": "برون‌خط", - "now": "اخیرن اینجا بوده", - "today": "آخرین بار درساعت {time} دیده شده", - "yesterday": "آخرین بار دیروز ساعت {time} دیده شده", - "at_day": "آخرین بار در تاریخ {date} دیده شده:", - "at_day_time": "آخرین بار در تاریخ {date} و ساعت {time} دیده شده", - "members": "{count} نفر" - }, - - "typing": { - "simple": "در حال نوشتن...", - "user": "{user} در حال نوشتن ...", - "group": { - "sequenced": "{users} نفر در حال نوشتن ...", - "many": "{count} نفر در حال نوشتن ..." - } - }, - - "content": { - - "unknown": "محتوای نامشخص", - "document": "سند", - "photo": "تصویر", - "video": "ویدیو", - "audio": "صدا", - "contact": "مخاطب", - "location": "موقعیت", - "sticker": "استیکر", - - "service": { - "registered": { - "full": "{name} به {app_name} پیوست", - "compact": "به {app_name} پیوست" - }, - "groups": { - "created": "{name} گروه را ایجاد کرد", - "invited": "{name} {name_added} را دعوت کرد", - "joined": "{name} به گروه پیوست", - "kicked": "{name} {name_kicked} را از گروه حذف کرد", - "left": "{name} از گروه رفت", - "title_changed": { - "full": "{name} نام گروه را با\"{title}\" جایگزین کرد", - "compact": "{name} نام گروه را جایگزین کرد" - }, - "topic_changed": { - "full": "جناب {name} موضوع گروه را با \"{topic}\" جای‌گزین کردید", - "compact": "موضوع گروه را عوض کرد {name}" - }, - "about_changed": { - "full": "جناب {name} متن درباره گروه را با \"{about}\" جای‌گزین کردید", - "compact": "متن درباره گروه را عوض کرد {name}" - }, - "avatar_changed": "{name} تصویر گروه را عوض کرد", - "avatar_removed": "{name} تصویر گروه را عوض کرد" - }, - "channels": { - "created": " گروه را ایجاد کرد", - "invited": "{name} {name_added} را دعوت کرد", - "joined": "{name} به گروه پیوست", - "kicked": "{name} {name_kicked} را از گروه حذف کرد", - "left": "{name} از گروه رفت", - "title_changed": { - "full": "{name} نام گروه را با\"{title}\" جایگزین کرد", - "compact": "{name} نام گروه را جایگزین کرد" - }, - "topic_changed": { - "full": "جناب {name} موضوع گروه را با \"{topic}\" جای‌گزین کردید", - "compact": "موضوع گروه را عوض کرد {name}" - }, - "about_changed": { - "full": "جناب {name} متن درباره گروه را با \"{about}\" جای‌گزین کردید", - "compact": "متن درباره گروه را عوض کرد {name}" - }, - "avatar_changed": "{name} تصویر گروه را عوض کرد", - "avatar_removed": "{name} تصویر گروه را عوض کرد" - }, - "calls": { - "missed": "تماس از دست رفته", - "ended": "تماس تمام شد" - } - } - }, - - "errors": { - "unknown": "Unknown error. Please, try again later.", - "internal": "خطای داخلی کارگزار. لطفن بعدن دوباره تلاش کنید", - "phone": { - "empty": "لطفن شماره تلفن خودتان را وارد کنید", - "incorrect": "شماره تلفن وارد شده معتبر نیست. لطفن دوباره تلاش کنید." - }, - "code": { - "empty": "کدتان معتبر نیست. لطفن دوباره تلاش کنید.", - "incorrect": "کد وارد شده معتبر نیست. لطفن دوباره تلاش کنید.", - "expired": "کدتان منقضی شده. لطفن دوباره از ابتدا شروع کنید." - }, - "groups": { - "already_joined": "شما هم‌اکنون نیز عضو گروه هستید.", - "unable_to_join": "امکان عضویت در گروه نیست" - } - }, - - "months": { - "january": "ژانویه", - "february": "فوریه", - "march": "مارس", - "april": "آوریل", - "may": "می", - "june": "ژوئن", - "july": "جولای", - "august": "آگوست", - "september": "سپتامبر", - "october": "اکتبر", - "november": "نوامبر", - "december": "دسامبر" - } + "app": { + "name": "Actor" + }, + "language": { + "code": "Fa", + "format": { + "time": { + "yesterday": "دیروز", + "now": "الآن", + "seconds": "{seconds} seconds", + "minutes": "{minutes} دقیقه", + "hours": "{hours} ساعت" + } + }, + "file_size": { + "bytes": "{bytes} B", + "kbytes": "{kbytes} KB", + "mbytes": "{mbytes} MB", + "gbytes": "{gbytes} GB" + }, + "referencing": { + "you": "شما", + "thee": "شما" + }, + "sequence": { + "or": ",", + "and": "," + } + }, + "groups": { + "members": "{count} نفر" + }, + "presence": { + "online": "برخط", + "offline": "برون‌خط", + "now": "اخیرن اینجا بوده", + "today": "آخرین بار درساعت {time} دیده شده", + "yesterday": "آخرین بار دیروز ساعت {time} دیده شده", + "at_day": "آخرین بار در تاریخ {date} دیده شده:", + "at_day_time": "آخرین بار در تاریخ {date} و ساعت {time} دیده شده", + "members": "{count} نفر" + }, + "typing": { + "simple": "در حال نوشتن...", + "user": "{user} در حال نوشتن ...", + "group": { + "sequenced": "{users} نفر در حال نوشتن ...", + "many": "{count} نفر در حال نوشتن ..." + } + }, + "content": { + "unknown": "محتوای نامشخص", + "document": "سند", + "photo": "تصویر", + "video": "ویدیو", + "audio": "صدا", + "contact": "مخاطب", + "location": "موقعیت", + "sticker": "استیکر", + "service": { + "registered": { + "full": "{name} به {app_name} پیوست", + "compact": "به {app_name} پیوست" + }, + "groups": { + "created": "{name} گروه را ایجاد کرد", + "invited": "{name} {name_added} را دعوت کرد", + "joined": "{name} به گروه پیوست", + "kicked": "{name} {name_kicked} را از گروه حذف کرد", + "left": "{name} از گروه رفت", + "title_changed": { + "full": "{name} نام گروه را با\"{title}\" جایگزین کرد", + "compact": "{name} نام گروه را جایگزین کرد" + }, + "topic_changed": { + "full": "جناب {name} موضوع گروه را با \"{topic}\" جای‌گزین کردید", + "compact": "موضوع گروه را عوض کرد {name}" + }, + "about_changed": { + "full": "جناب {name} متن درباره گروه را با \"{about}\" جای‌گزین کردید", + "compact": "متن درباره گروه را عوض کرد {name}" + }, + "avatar_changed": "{name} تصویر گروه را عوض کرد", + "avatar_removed": "{name} تصویر گروه را عوض کرد" + }, + "channels": { + "created": " گروه را ایجاد کرد", + "invited": "{name} {name_added} را دعوت کرد", + "joined": "{name} به گروه پیوست", + "kicked": "{name} {name_kicked} را از گروه حذف کرد", + "left": "{name} از گروه رفت", + "title_changed": { + "full": "{name} نام گروه را با\"{title}\" جایگزین کرد", + "compact": "{name} نام گروه را جایگزین کرد" + }, + "topic_changed": { + "full": "جناب {name} موضوع گروه را با \"{topic}\" جای‌گزین کردید", + "compact": "موضوع گروه را عوض کرد {name}" + }, + "about_changed": { + "full": "جناب {name} متن درباره گروه را با \"{about}\" جای‌گزین کردید", + "compact": "متن درباره گروه را عوض کرد {name}" + }, + "avatar_changed": "{name} تصویر گروه را عوض کرد", + "avatar_removed": "{name} تصویر گروه را عوض کرد" + }, + "calls": { + "missed": "تماس از دست رفته", + "ended": "تماس تمام شد" + }, + "encrypted": { + "timer_changed": { + "full": "{name} set self-destruct timer to {timer}", + "compact": "{name} set self-destruct timer" + }, + "timer_disabled": "{name} disabled self-destruct timer" + } + } + }, + "errors": { + "unknown": "Unknown error. Please, try again later.", + "internal": "خطای داخلی کارگزار. لطفن بعدن دوباره تلاش کنید", + "phone": { + "empty": "لطفن شماره تلفن خودتان را وارد کنید", + "incorrect": "شماره تلفن وارد شده معتبر نیست. لطفن دوباره تلاش کنید." + }, + "code": { + "empty": "کدتان معتبر نیست. لطفن دوباره تلاش کنید.", + "incorrect": "کد وارد شده معتبر نیست. لطفن دوباره تلاش کنید.", + "expired": "کدتان منقضی شده. لطفن دوباره از ابتدا شروع کنید." + }, + "groups": { + "already_joined": "شما هم‌اکنون نیز عضو گروه هستید.", + "unable_to_join": "امکان عضویت در گروه نیست" + } + }, + "months": { + "january": "ژانویه", + "february": "فوریه", + "march": "مارس", + "april": "آوریل", + "may": "می", + "june": "ژوئن", + "july": "جولای", + "august": "آگوست", + "september": "سپتامبر", + "october": "اکتبر", + "november": "نوامبر", + "december": "دسامبر" + } } \ No newline at end of file diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/resources/AppText_Pt.json b/actor-sdk/sdk-core/core/core-shared/src/main/resources/AppText_Pt.json index f42f5e152e..23d11714e7 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/resources/AppText_Pt.json +++ b/actor-sdk/sdk-core/core/core-shared/src/main/resources/AppText_Pt.json @@ -1,206 +1,203 @@ { - "app": { - "name": "Actor" - }, - - "language": { - - "code": "Pt", - - "format": { - "time": { - "yesterday":{ - "short": "Ontem", - "full": "Ontem" - }, - "now": "Agora", - "minutes": { - "short": "{minutes} min", - "full": "{minutes} min" - }, - "hours": { - "short": "{hours} hs", - "full": "{hours} hs" - } - } - }, - - "file_size": { - "bytes": "{bytes} B", - "kbytes": "{kbytes} KB", - "mbytes": "{mbytes} MB", - "gbytes": "{gbytes} GB" - }, - - "referencing": { - "you": "Você", - "thee": "Você" - }, - - "sequence": { - "or": ", ", - "and": " and " - } - }, - - "groups": { - "members": { - "other": "{count} membros", - "one": "{count} membro" - } - }, - - "presence": { - "online": "online", - "offline": "offline", - "now": "visto agora mesmo", - "today": "visto hoje às {time}", - "yesterday": "visto ontem às {time}", - "at_day": "visto {date}", - "at_day_time": "visto {date} às {time}", - "members": "{count} online" - }, - - "typing": { - "simple": "digitando...", - "user": "{user} está digitando...", - "group": { - "sequenced": "{users} estão digitando...", - "many": "{count} pessoas estão digitando..." - } - }, - - "content": { - - "unknown": "Conteúdo não suportado", - "document": "Documento", - "photo": "Foto", - "video": "Vídeo", - "audio": "Audio", - "contact": "Contact", - "location": "Location", - "sticker": "Sticker", - - "service": { - "registered": { - "full": "{name} entrou na Rede {app_name}", - "compact": "Entrou no {app_name}" - }, - "groups": { - "created": "{name} criou o grupo", - "invited": "{name} convidado {name_added}", - "joined": "{name} entrou no grupo", - "kicked": "{name} removido {name_kicked}", - "left": "{name} saiu do grupo", - "title_changed": { - "full": "{name} alterou o nome do grupo para \"{title}\"", - "compact": "{name} alterou o nome do grupo" - }, - "topic_changed": { - "full": "{name} changed the group topic to \"{topic}\"", - "compact": "{name} changed the group topic" - }, - "about_changed": { - "full": "{name} changed the group about to \"{about}\"", - "compact": "{name} changed the group about" - }, - "avatar_changed": "{name} alterou a foto do grupo", - "avatar_removed": "{name} removeu a foto do grupo" - }, - "channels": { - "created": "Criou o canal", - "invited": "{name} convidado {name_added}", - "joined": "{name} entrou no canal", - "kicked": "{name} removido {name_kicked}", - "left": "{name} saiu do canal", - "title_changed": { - "full": "{name} alterou o nome do canal para \"{title}\"", - "compact": "{name} alterou o nome do canal" - }, - "topic_changed": { - "full": "{name} changed the canal topic to \"{topic}\"", - "compact": "{name} changed the canal topic" - }, - "about_changed": { - "full": "{name} changed the canal about to \"{about}\"", - "compact": "{name} changed the canal about" - }, - "avatar_changed": "{name} alterou a foto do canal", - "avatar_removed": "{name} removeu a foto do canal" - }, - "calls": { - "missed": "Missed call", - "ended": "Call ended" - } - } - }, - - "errors": { - "unknown": "Unknown error. Please, try again later.", - "internal": "Internal server error. Please, try again later.", - "phone": { - "empty": "Please, enter your phone number.", - "incorrect": "Entered phone number is invalid. Please, try again." - }, - "code": { - "empty": "The code is invalid. Please try again.", - "incorrect": "Entered code is incorrect. Please, try again.", - "expired": "The code has expired. Please, start authentication again." - }, - "groups": { - "already_joined": "You have already joined the group.", - "unable_to_join": "Unable to join the group." - } - }, - - "months": { - "january": { - "compact": "jan", - "full":"janeiro" - }, - "february": { - "compact": "fev", - "full":"fevereiro" - }, - "march": { - "compact": "mar", - "full":"março" - }, - "april": { - "compact": "abr", - "full":"abril" - }, - "may": { - "compact": "mai", - "full":"maio" - }, - "june": { - "compact": "jun", - "full":"junho" - }, - "july": { - "compact": "jul", - "full":"julho" - }, - "august": { - "compact": "ago", - "full":"agosto" - }, - "september": { - "compact": "set", - "full":"setembro" - }, - "october": { - "compact": "out", - "full":"outubro" - }, - "november": { - "compact": "nov", - "full":"novembro" - }, - "december": { - "compact": "dez", - "full":"dezembro" - } - } + "app": { + "name": "Actor" + }, + "language": { + "code": "Pt", + "format": { + "time": { + "yesterday": { + "short": "Ontem", + "full": "Ontem" + }, + "now": "Agora", + "seconds": { + "short": "{seconds} sec", + "full": "{seconds} seconds" + }, + "minutes": { + "short": "{minutes} min", + "full": "{minutes} minutes" + }, + "hours": { + "short": "{hours} hs", + "full": "{hours} hs" + } + } + }, + "file_size": { + "bytes": "{bytes} B", + "kbytes": "{kbytes} KB", + "mbytes": "{mbytes} MB", + "gbytes": "{gbytes} GB" + }, + "referencing": { + "you": "Você", + "thee": "Você" + }, + "sequence": { + "or": ", ", + "and": " and " + } + }, + "groups": { + "members": { + "other": "{count} membros", + "one": "{count} membro" + } + }, + "presence": { + "online": "online", + "offline": "offline", + "now": "visto agora mesmo", + "today": "visto hoje às {time}", + "yesterday": "visto ontem às {time}", + "at_day": "visto {date}", + "at_day_time": "visto {date} às {time}", + "members": "{count} online" + }, + "typing": { + "simple": "digitando...", + "user": "{user} está digitando...", + "group": { + "sequenced": "{users} estão digitando...", + "many": "{count} pessoas estão digitando..." + } + }, + "content": { + "unknown": "Conteúdo não suportado", + "document": "Documento", + "photo": "Foto", + "video": "Vídeo", + "audio": "Audio", + "contact": "Contact", + "location": "Location", + "sticker": "Sticker", + "service": { + "registered": { + "full": "{name} entrou na Rede {app_name}", + "compact": "Entrou no {app_name}" + }, + "groups": { + "created": "{name} criou o grupo", + "invited": "{name} convidado {name_added}", + "joined": "{name} entrou no grupo", + "kicked": "{name} removido {name_kicked}", + "left": "{name} saiu do grupo", + "title_changed": { + "full": "{name} alterou o nome do grupo para \"{title}\"", + "compact": "{name} alterou o nome do grupo" + }, + "topic_changed": { + "full": "{name} changed the group topic to \"{topic}\"", + "compact": "{name} changed the group topic" + }, + "about_changed": { + "full": "{name} changed the group about to \"{about}\"", + "compact": "{name} changed the group about" + }, + "avatar_changed": "{name} alterou a foto do grupo", + "avatar_removed": "{name} removeu a foto do grupo" + }, + "channels": { + "created": "Criou o canal", + "invited": "{name} convidado {name_added}", + "joined": "{name} entrou no canal", + "kicked": "{name} removido {name_kicked}", + "left": "{name} saiu do canal", + "title_changed": { + "full": "{name} alterou o nome do canal para \"{title}\"", + "compact": "{name} alterou o nome do canal" + }, + "topic_changed": { + "full": "{name} changed the canal topic to \"{topic}\"", + "compact": "{name} changed the canal topic" + }, + "about_changed": { + "full": "{name} changed the canal about to \"{about}\"", + "compact": "{name} changed the canal about" + }, + "avatar_changed": "{name} alterou a foto do canal", + "avatar_removed": "{name} removeu a foto do canal" + }, + "calls": { + "missed": "Missed call", + "ended": "Call ended" + }, + "encrypted": { + "timer_changed": { + "full": "{name} set self-destruct timer to {timer}", + "compact": "{name} set self-destruct timer" + }, + "timer_disabled": "{name} disabled self-destruct timer" + } + } + }, + "errors": { + "unknown": "Unknown error. Please, try again later.", + "internal": "Internal server error. Please, try again later.", + "phone": { + "empty": "Please, enter your phone number.", + "incorrect": "Entered phone number is invalid. Please, try again." + }, + "code": { + "empty": "The code is invalid. Please try again.", + "incorrect": "Entered code is incorrect. Please, try again.", + "expired": "The code has expired. Please, start authentication again." + }, + "groups": { + "already_joined": "You have already joined the group.", + "unable_to_join": "Unable to join the group." + } + }, + "months": { + "january": { + "compact": "jan", + "full": "janeiro" + }, + "february": { + "compact": "fev", + "full": "fevereiro" + }, + "march": { + "compact": "mar", + "full": "março" + }, + "april": { + "compact": "abr", + "full": "abril" + }, + "may": { + "compact": "mai", + "full": "maio" + }, + "june": { + "compact": "jun", + "full": "junho" + }, + "july": { + "compact": "jul", + "full": "julho" + }, + "august": { + "compact": "ago", + "full": "agosto" + }, + "september": { + "compact": "set", + "full": "setembro" + }, + "october": { + "compact": "out", + "full": "outubro" + }, + "november": { + "compact": "nov", + "full": "novembro" + }, + "december": { + "compact": "dez", + "full": "dezembro" + } + } } \ No newline at end of file diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/resources/AppText_Ru.json b/actor-sdk/sdk-core/core/core-shared/src/main/resources/AppText_Ru.json index 0f8bc7a55e..b9d0bf5c96 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/resources/AppText_Ru.json +++ b/actor-sdk/sdk-core/core/core-shared/src/main/resources/AppText_Ru.json @@ -1,366 +1,393 @@ { - "app": { - "name": "Actor" - }, - - "language": { - - "code": "Ru", - - "format": { - "time": { - "yesterday":{ - "short": "Вчера", - "full": "Вчера" - }, - "now": "Сейчас", - "minutes": { - "short": "{minutes} мин.", - "full": "{minutes} минут" - }, - "hours": { - "short": "{hours} ч.", - "full": "{hours} часов" - } - } - }, - - "file_size": { - "bytes": "{bytes} б", - "kbytes": "{kbytes} кб", - "mbytes": "{mbytes} мб", - "gbytes": "{gbytes} гб" - }, - - "referencing": { - "you": "Вы", - "thee": "Вас" - }, - - "sequence": { - "or": ", ", - "and": " и " - } - }, - - "groups": { - "members": { - "other": "{count} участников", - "one": "{count} участник", - "few": "{count} участника", - "many": "{count} участников" - } - }, - - "presence": { - "online": "в сети", - "offline": "не в сети", - "now": { - "male": "был только что", - "female": "была только что", - "other": "был(а) только что" - }, - "today": { - "male": "был сегодня в {time}", - "female": "была сегодня в {time}", - "other": "был(а) сегодня в {time}" - }, - "yesterday": { - "male": "был вчера в {time}", - "female": "была вчера в {time}", - "other": "был(а) вчера в {time}" - }, - "at_day": { - "male": "был {date}", - "female": "была {date}", - "other": "был(а) {date}" - }, - "at_day_time": { - "male": "был {date} в {time}", - "female": "была {date} в {time}", - "other": "был(а) {date} в {time}" - }, - "members": "{count} в сети" - }, - - "typing": { - "simple": "печатает...", - "user": "{user} печатает...", - "group": { - "sequenced": "{users} печатают...", - "one": "{count} печатает...", - "few": "{count} печатают...", - "many": "{count} печатает...", - "other": "{count} печатает..." - } - }, - - "content": { - - "unknown": "Неподдерживаемое сообщение", - "document": "Документ", - "photo": "Фото", - "video": "Видео", - "audio": "Аудио", - "contact": "Контакт", - "location": "Расположение", - "sticker": "Стикер", - - "service": { - "registered": { - "full": { - "you": "{name} зарегистрировались в {app_name}", - "male": "{name} зарегистрировался в {app_name}", - "female": "{name} зарегистрировалась в {app_name}", - "other": "{name} зарегистрировался(ась) в {app_name}" - }, - "compact": { - "you": "Вы зарегистрироваись в {app_name}", - "male": "Зарегистрировался в {app_name}", - "female": "Зарегистрировалась в {app_name}", - "other": "Зарегистрировался(ась) в {app_name}" - } - }, - "groups": { - "created": { - "you": "{name} создали группу", - "male": "{name} создал группу", - "female": "{name} создала группу", - "other": "{name} создал(-а) группу" - }, - "invited": { - "you": "{name} пригласили {name_added}", - "male": "{name} пригласил {name_added}", - "female": "{name} пригласила {name_added}", - "other": "{name} пригласил(-а) {name_added}" - }, - "joined": { - "you": "{name} вошли в группу", - "male": "{name} вошел в группу", - "female": "{name} вошла в группу", - "other": "{name} вошел(-шла) в группу" - }, - "kicked": { - "you": "{name} исключили {name_kicked}", - "male": "{name} исключил {name_kicked}", - "female": "{name} исключила {name_kicked}", - "other": "{name} исключил(-а) {name_kicked}" - }, - "left": { - "you": "{name} покинули группу", - "male": "{name} покинул группу", - "female": "{name} покинула группу", - "other": "{name} покинул(-а) группу" - }, - "title_changed": { - "full": { - "you": "{name} изменили название группы на \"{title}\"", - "male": "{name} изменил название группы на \"{title}\"", - "female": "{name} изменила название группы на \"{title}\"", - "other": "{name} изменил(-а) название группы на \"{title}\"" - }, - "compact": { - "you": "{name} изменили название группы", - "male": "{name} изменил название группы", - "female": "{name} изменила название группы", - "other": "{name} изменил(-а) название группы" - } - }, - "topic_changed": { - "full": { - "you": "{name} изменили тему группы на \"{title}\"", - "male": "{name} изменил тему группы на \"{title}\"", - "female": "{name} изменила тему группы на \"{title}\"", - "other": "{name} изменил(-а) тему группы на \"{title}\"" - }, - "compact": { - "you": "{name} изменили тему группы", - "male": "{name} изменил тему группы", - "female": "{name} изменила тему группы", - "other": "{name} изменил(-а) тему группы" - } - }, - "about_changed": { - "full": { - "you": "{name} изменили описание группы на \"{title}\"", - "male": "{name} изменил описание группы на \"{title}\"", - "female": "{name} изменила описание группы на \"{title}\"", - "other": "{name} изменил(-а) описание группы на \"{title}\"" - }, - "compact": { - "you": "{name} изменили описание группы", - "male": "{name} изменил описание группы", - "female": "{name} изменила описание группы", - "other": "{name} изменил(-а) описание группы" - } - }, - "avatar_changed": { - "you": "{name} изменили фото группы", - "male": "{name} изменил фото группы", - "female": "{name} изменила фото группы", - "other": "{name} изменил(-а) фото группы" - }, - "avatar_removed": { - "you": "{name} удалили фото группы", - "male": "{name} удалил фото группы", - "female": "{name} удалила фото группы", - "other": "{name} удалил(-а) фото группы" - } - }, - "channels": { - "created": "Создан канал", - "invited": { - "you": "{name} пригласили {name_added}", - "male": "{name} пригласил {name_added}", - "female": "{name} пригласила {name_added}", - "other": "{name} пригласил(-а) {name_added}" - }, - "joined": { - "you": "{name} вошли в канал", - "male": "{name} вошел в канал", - "female": "{name} вошла в канал", - "other": "{name} вошел(-шла) в канал" - }, - "kicked": { - "you": "{name} исключили {name_kicked}", - "male": "{name} исключил {name_kicked}", - "female": "{name} исключила {name_kicked}", - "other": "{name} исключил(-а) {name_kicked}" - }, - "left": { - "you": "{name} покинули канал", - "male": "{name} покинул канал", - "female": "{name} покинула канал", - "other": "{name} покинул(-а) канал" - }, - "title_changed": { - "full": { - "you": "{name} изменили название канала на \"{title}\"", - "male": "{name} изменил название канала на \"{title}\"", - "female": "{name} изменила название канала на \"{title}\"", - "other": "{name} изменил(-а) название канала на \"{title}\"" - }, - "compact": { - "you": "{name} изменили название канала", - "male": "{name} изменил название канала", - "female": "{name} изменила название канала", - "other": "{name} изменил(-а) название канала" - } - }, - "topic_changed": { - "full": { - "you": "{name} изменили тему канала на \"{title}\"", - "male": "{name} изменил тему канала на \"{title}\"", - "female": "{name} изменила тему канала на \"{title}\"", - "other": "{name} изменил(-а) тему канала на \"{title}\"" - }, - "compact": { - "you": "{name} изменили тему канала", - "male": "{name} изменил тему канала", - "female": "{name} изменила тему канала", - "other": "{name} изменил(-а) тему канала" - } - }, - "about_changed": { - "full": { - "you": "{name} изменили описание канала на \"{title}\"", - "male": "{name} изменил описание канала на \"{title}\"", - "female": "{name} изменила описание канала на \"{title}\"", - "other": "{name} изменил(-а) описание канала на \"{title}\"" - }, - "compact": { - "you": "{name} изменили канала канала", - "male": "{name} изменил описание канала", - "female": "{name} изменила описание канала", - "other": "{name} изменил(-а) описание канала" - } - }, - "avatar_changed": { - "you": "{name} изменили фото канала", - "male": "{name} изменил фото канала", - "female": "{name} изменила фото канала", - "other": "{name} изменил(-а) фото канала" - }, - "avatar_removed": { - "you": "{name} удалили фото канала", - "male": "{name} удалил фото канала", - "female": "{name} удалила фото канала", - "other": "{name} удалил(-а) фото канала" - } - }, - "calls": { - "missed": "Пропущен вызов", - "ended": "Вызов завершен" - } - } - }, - - "errors": { - "unknown": "Неизвестная ошибка. Пожалуйста, попробуйте еще раз.", - "internal": "Внутренняя ошибка сервера. Пожалуйста, попробуйте ещё раз.", - "phone": { - "empty": "Пожалуйста, введите свой номер телефона.", - "incorrect": "Введён некорректный номер телефона. Пожалуйста, попробуйте ещё раз." - }, - "code": { - "empty": "Пожалуйста, введите код подтверждения.", - "incorrect": "Введён некорректный код. Пожалуйста, попробуйте ещё раз.", - "expired": "Срок действия кода истёк. Пожалуйста, авторизуйтесь заново." - }, - "groups": { - "already_joined": "Вы уже вступили в эту группу.", - "unable_to_join": "Невозможно присоединиться к группе." - } - }, - - "months": { - "january": { - "compact": "янв", - "full":"январь" - }, - "february": { - "compact": "фев", - "full":"февраль" - }, - "march": { - "compact": "мар", - "full":"март" - }, - "april": { - "compact": "апр", - "full":"апрель" - }, - "may": { - "compact": "май", - "full":"май" - }, - "june": { - "compact": "июн", - "full":"июнь" - }, - "july": { - "compact": "июл", - "full":"июль" - }, - "august": { - "compact": "авг", - "full":"август" - }, - "september": { - "compact": "сен", - "full":"сентябрь" - }, - "october": { - "compact": "окт", - "full":"октябрь" - }, - "november": { - "compact": "ноя", - "full":"ноябрь" - }, - "december": { - "compact": "дек", - "full":"декабрь" - } - } + "app": { + "name": "Actor" + }, + "language": { + "code": "Ru", + "format": { + "time": { + "yesterday": { + "short": "Вчера", + "full": "Вчера" + }, + "now": "Сейчас", + "seconds": { + "short": "{seconds} с.", + "full": { + "other": "{seconds} секунд", + "one": "{seconds} секунда", + "few": "{seconds} секунды", + "many": "{seconds} секунд" + } + }, + "minutes": { + "short": "{minutes} мин.", + "full": { + "other": "{minutes} минут", + "one": "{minutes} минута", + "few": "{minutes} минуты", + "many": "{minutes} минут" + } + }, + "hours": { + "short": "{hours} ч.", + "full": { + "other": "{hours} часов", + "one": "{hours} час", + "few": "{hours} часа", + "many": "{hours} часов" + } + } + } + }, + "file_size": { + "bytes": "{bytes} б", + "kbytes": "{kbytes} кб", + "mbytes": "{mbytes} мб", + "gbytes": "{gbytes} гб" + }, + "referencing": { + "you": "Вы", + "thee": "Вас" + }, + "sequence": { + "or": ", ", + "and": " и " + } + }, + "groups": { + "members": { + "other": "{count} участников", + "one": "{count} участник", + "few": "{count} участника", + "many": "{count} участников" + } + }, + "presence": { + "online": "в сети", + "offline": "не в сети", + "now": { + "male": "был только что", + "female": "была только что", + "other": "был(а) только что" + }, + "today": { + "male": "был сегодня в {time}", + "female": "была сегодня в {time}", + "other": "был(а) сегодня в {time}" + }, + "yesterday": { + "male": "был вчера в {time}", + "female": "была вчера в {time}", + "other": "был(а) вчера в {time}" + }, + "at_day": { + "male": "был {date}", + "female": "была {date}", + "other": "был(а) {date}" + }, + "at_day_time": { + "male": "был {date} в {time}", + "female": "была {date} в {time}", + "other": "был(а) {date} в {time}" + }, + "members": "{count} в сети" + }, + "typing": { + "simple": "печатает...", + "user": "{user} печатает...", + "group": { + "sequenced": "{users} печатают...", + "one": "{count} печатает...", + "few": "{count} печатают...", + "many": "{count} печатает...", + "other": "{count} печатает..." + } + }, + "content": { + "unknown": "Неподдерживаемое сообщение", + "document": "Документ", + "photo": "Фото", + "video": "Видео", + "audio": "Аудио", + "contact": "Контакт", + "location": "Расположение", + "sticker": "Стикер", + "service": { + "registered": { + "full": { + "you": "{name} зарегистрировались в {app_name}", + "male": "{name} зарегистрировался в {app_name}", + "female": "{name} зарегистрировалась в {app_name}", + "other": "{name} зарегистрировался(ась) в {app_name}" + }, + "compact": { + "you": "Вы зарегистрироваись в {app_name}", + "male": "Зарегистрировался в {app_name}", + "female": "Зарегистрировалась в {app_name}", + "other": "Зарегистрировался(ась) в {app_name}" + } + }, + "groups": { + "created": { + "you": "{name} создали группу", + "male": "{name} создал группу", + "female": "{name} создала группу", + "other": "{name} создал(а) группу" + }, + "invited": { + "you": "{name} пригласили {name_added}", + "male": "{name} пригласил {name_added}", + "female": "{name} пригласила {name_added}", + "other": "{name} пригласил(а) {name_added}" + }, + "joined": { + "you": "{name} вошли в группу", + "male": "{name} вошел в группу", + "female": "{name} вошла в группу", + "other": "{name} вошел(шла) в группу" + }, + "kicked": { + "you": "{name} исключили {name_kicked}", + "male": "{name} исключил {name_kicked}", + "female": "{name} исключила {name_kicked}", + "other": "{name} исключил(а) {name_kicked}" + }, + "left": { + "you": "{name} покинули группу", + "male": "{name} покинул группу", + "female": "{name} покинула группу", + "other": "{name} покинул(а) группу" + }, + "title_changed": { + "full": { + "you": "{name} изменили название группы на \"{title}\"", + "male": "{name} изменил название группы на \"{title}\"", + "female": "{name} изменила название группы на \"{title}\"", + "other": "{name} изменил(а) название группы на \"{title}\"" + }, + "compact": { + "you": "{name} изменили название группы", + "male": "{name} изменил название группы", + "female": "{name} изменила название группы", + "other": "{name} изменил(а) название группы" + } + }, + "topic_changed": { + "full": { + "you": "{name} изменили тему группы на \"{title}\"", + "male": "{name} изменил тему группы на \"{title}\"", + "female": "{name} изменила тему группы на \"{title}\"", + "other": "{name} изменил(а) тему группы на \"{title}\"" + }, + "compact": { + "you": "{name} изменили тему группы", + "male": "{name} изменил тему группы", + "female": "{name} изменила тему группы", + "other": "{name} изменил(а) тему группы" + } + }, + "about_changed": { + "full": { + "you": "{name} изменили описание группы на \"{title}\"", + "male": "{name} изменил описание группы на \"{title}\"", + "female": "{name} изменила описание группы на \"{title}\"", + "other": "{name} изменил(а) описание группы на \"{title}\"" + }, + "compact": { + "you": "{name} изменили описание группы", + "male": "{name} изменил описание группы", + "female": "{name} изменила описание группы", + "other": "{name} изменил(а) описание группы" + } + }, + "avatar_changed": { + "you": "{name} изменили фото группы", + "male": "{name} изменил фото группы", + "female": "{name} изменила фото группы", + "other": "{name} изменил(а) фото группы" + }, + "avatar_removed": { + "you": "{name} удалили фото группы", + "male": "{name} удалил фото группы", + "female": "{name} удалила фото группы", + "other": "{name} удалил(а) фото группы" + } + }, + "channels": { + "created": "Создан канал", + "invited": { + "you": "{name} пригласили {name_added}", + "male": "{name} пригласил {name_added}", + "female": "{name} пригласила {name_added}", + "other": "{name} пригласил(а) {name_added}" + }, + "joined": { + "you": "{name} вошли в канал", + "male": "{name} вошел в канал", + "female": "{name} вошла в канал", + "other": "{name} вошел(шла) в канал" + }, + "kicked": { + "you": "{name} исключили {name_kicked}", + "male": "{name} исключил {name_kicked}", + "female": "{name} исключила {name_kicked}", + "other": "{name} исключил(а) {name_kicked}" + }, + "left": { + "you": "{name} покинули канал", + "male": "{name} покинул канал", + "female": "{name} покинула канал", + "other": "{name} покинул(а) канал" + }, + "title_changed": { + "full": { + "you": "{name} изменили название канала на \"{title}\"", + "male": "{name} изменил название канала на \"{title}\"", + "female": "{name} изменила название канала на \"{title}\"", + "other": "{name} изменил(а) название канала на \"{title}\"" + }, + "compact": { + "you": "{name} изменили название канала", + "male": "{name} изменил название канала", + "female": "{name} изменила название канала", + "other": "{name} изменил(а) название канала" + } + }, + "topic_changed": { + "full": { + "you": "{name} изменили тему канала на \"{title}\"", + "male": "{name} изменил тему канала на \"{title}\"", + "female": "{name} изменила тему канала на \"{title}\"", + "other": "{name} изменил(а) тему канала на \"{title}\"" + }, + "compact": { + "you": "{name} изменили тему канала", + "male": "{name} изменил тему канала", + "female": "{name} изменила тему канала", + "other": "{name} изменил(а) тему канала" + } + }, + "about_changed": { + "full": { + "you": "{name} изменили описание канала на \"{title}\"", + "male": "{name} изменил описание канала на \"{title}\"", + "female": "{name} изменила описание канала на \"{title}\"", + "other": "{name} изменил(а) описание канала на \"{title}\"" + }, + "compact": { + "you": "{name} изменили канала канала", + "male": "{name} изменил описание канала", + "female": "{name} изменила описание канала", + "other": "{name} изменил(а) описание канала" + } + }, + "avatar_changed": { + "you": "{name} изменили фото канала", + "male": "{name} изменил фото канала", + "female": "{name} изменила фото канала", + "other": "{name} изменил(а) фото канала" + }, + "avatar_removed": { + "you": "{name} удалили фото канала", + "male": "{name} удалил фото канала", + "female": "{name} удалила фото канала", + "other": "{name} удалил(а) фото канала" + } + }, + "calls": { + "missed": "Пропущен вызов", + "ended": "Вызов завершен" + }, + "encrypted": { + "timer_changed": { + "full": { + "you": "{name} установили таймер самоуничтожения: {timer}", + "male": "{name} установил таймер самоуничтожения: {timer}", + "female": "{name} установила таймер самоуничтожения: {timer}", + "other": "{name} установил(а) таймер самоуничтожения: {timer}" + }, + "compact": { + "you": "{name} установили таймер самоуничтожения", + "male": "{name} установил таймер самоуничтожения", + "female": "{name} установила таймер самоуничтожения", + "other": "{name} установил(а) таймер самоуничтожения" + } + }, + "timer_disabled": { + "you": "{name} отключили таймер самоуничтожения", + "male": "{name} отключил таймер самоуничтожения", + "female": "{name} отключила таймер самоуничтожения", + "other": "{name} отключил(а) таймер самоуничтожения" + } + } + } + }, + "errors": { + "unknown": "Неизвестная ошибка. Пожалуйста, попробуйте еще раз.", + "internal": "Внутренняя ошибка сервера. Пожалуйста, попробуйте ещё раз.", + "phone": { + "empty": "Пожалуйста, введите свой номер телефона.", + "incorrect": "Введён некорректный номер телефона. Пожалуйста, попробуйте ещё раз." + }, + "code": { + "empty": "Пожалуйста, введите код подтверждения.", + "incorrect": "Введён некорректный код. Пожалуйста, попробуйте ещё раз.", + "expired": "Срок действия кода истёк. Пожалуйста, авторизуйтесь заново." + }, + "groups": { + "already_joined": "Вы уже вступили в эту группу.", + "unable_to_join": "Невозможно присоединиться к группе." + } + }, + "months": { + "january": { + "compact": "янв", + "full": "январь" + }, + "february": { + "compact": "фев", + "full": "февраль" + }, + "march": { + "compact": "мар", + "full": "март" + }, + "april": { + "compact": "апр", + "full": "апрель" + }, + "may": { + "compact": "май", + "full": "май" + }, + "june": { + "compact": "июн", + "full": "июнь" + }, + "july": { + "compact": "июл", + "full": "июль" + }, + "august": { + "compact": "авг", + "full": "август" + }, + "september": { + "compact": "сен", + "full": "сентябрь" + }, + "october": { + "compact": "окт", + "full": "октябрь" + }, + "november": { + "compact": "ноя", + "full": "ноябрь" + }, + "december": { + "compact": "дек", + "full": "декабрь" + } + } } \ No newline at end of file diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/resources/AppText_Zn.json b/actor-sdk/sdk-core/core/core-shared/src/main/resources/AppText_Zn.json index f253e8b0ee..5fab7b757c 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/resources/AppText_Zn.json +++ b/actor-sdk/sdk-core/core/core-shared/src/main/resources/AppText_Zn.json @@ -1,164 +1,161 @@ { - "app": { - "name": "优聆" - }, - - "language": { - - "code": "Zn", - - "format": { - "time": { - "yesterday": "昨天", - "now": "现在", - "minutes": { - "short": "{minutes} 分", - "full": "{minutes} 分" - }, - "hours": { - "short": "{hours} 小时", - "full": "{hours} 小时" - } - } - }, - - "file_size": { - "bytes": "{bytes} B", - "kbytes": "{kbytes} KB", - "mbytes": "{mbytes} MB", - "gbytes": "{gbytes} GB" - }, - - "referencing": { - "you": "你", - "thee": "你" - }, - - "sequence": { - "or": ",", - "and": "," - } - }, - - "groups": { - "members": "{count}个成员" - }, - - "presence": { - "online": "在线", - "offline": "离线", - "now": "刚刚最后一次上线", - "today": "今天{time}最后一次上线", - "yesterday": "昨天{time}最后一次上线", - "at_day": "{date}最后一次上线", - "at_day_time": "{date} {time}最后一次上线", - "members": "{count}人在线" - }, - - "typing": { - "simple": "正在输入...", - "user": "{user}正在输入...", - "group": { - "sequenced": "{users}正在输入...", - "many": "{count}个人正在输入..." - } - }, - - "content": { - - "unknown": "不支持的内容", - "document": "文档", - "photo": "照片", - "video": "图片", - "audio": "语音", - "contact": "通讯录", - "location": "位置", - "sticker": "贴纸", - - "service": { - "registered": { - "full": "{name} 已加入 {app_name}", - "compact": "已加入 {app_name}" - }, - "groups": { - "created": "{name}创建了这个群组", - "invited": "{name}邀请{name_added}", - "joined": "{name}加入群组", - "kicked": "{name}移出{name_kicked}", - "left": "{name}退出群组", - "title_changed": { - "full": "{name}修改为名称为:\"{title}\"", - "compact": "{name}修改了群组名称" - }, - "topic_changed": { - "full": "{name} changed the group topic to \"{topic}\"", - "compact": "{name} changed the group topic" - }, - "about_changed": { - "full": "{name} changed the group about to \"{about}\"", - "compact": "{name} changed the group about" - }, - "avatar_changed": "{name}修改了群组头像", - "avatar_removed": "{name}删除了群组头像" - }, - "channels": { - "created": "{name}创建了这个群组", - "invited": "{name}邀请{name_added}", - "joined": "{name}加入群组", - "kicked": "{name}移出{name_kicked}", - "left": "{name}退出群组", - "title_changed": { - "full": "{name}修改为名称为:\"{title}\"", - "compact": "{name}修改了群组名称" - }, - "topic_changed": { - "full": "{name} changed the group topic to \"{topic}\"", - "compact": "{name} changed the group topic" - }, - "about_changed": { - "full": "{name} changed the group about to \"{about}\"", - "compact": "{name} changed the group about" - }, - "avatar_changed": "{name}修改了群组头像", - "avatar_removed": "{name}删除了群组头像" - }, - "calls": { - "missed": "未接电话", - "ended": "拨号结束" - } - } - }, - - "errors": { - "unknown": "Unknown error. Please, try again later.", - "internal": "服务器内部错误,请稍候再试。", - "phone": { - "empty": "请输入你的手机号码", - "incorrect": "输入的手机号码不正确,请重新输入。" - }, - "code": { - "empty": "请正确输入验证码。", - "incorrect": "输入的验证码不正确,请重试。", - "expired": "验证码过期,请重新验证。" - }, - "groups": { - "already_joined": "你已经是群组成员。", - "unable_to_join": "无法加入群组" - } - }, - - "months": { - "january": "一月", - "february": "二月", - "march": "三月", - "april": "四月", - "may": "五月", - "june": "六月", - "july": "七月", - "august": "八月", - "september": "九月", - "october": "十月", - "november": "十一月", - "december": "十二月" - } + "app": { + "name": "优聆" + }, + "language": { + "code": "Zn", + "format": { + "time": { + "yesterday": "昨天", + "now": "现在", + "seconds": { + "short": "{seconds} 第二", + "full": "{seconds} 第二" + }, + "minutes": { + "short": "{minutes} 分", + "full": "{minutes} 分" + }, + "hours": { + "short": "{hours} 小时", + "full": "{hours} 小时" + } + } + }, + "file_size": { + "bytes": "{bytes} B", + "kbytes": "{kbytes} KB", + "mbytes": "{mbytes} MB", + "gbytes": "{gbytes} GB" + }, + "referencing": { + "you": "你", + "thee": "你" + }, + "sequence": { + "or": ",", + "and": "," + } + }, + "groups": { + "members": "{count}个成员" + }, + "presence": { + "online": "在线", + "offline": "离线", + "now": "刚刚最后一次上线", + "today": "今天{time}最后一次上线", + "yesterday": "昨天{time}最后一次上线", + "at_day": "{date}最后一次上线", + "at_day_time": "{date} {time}最后一次上线", + "members": "{count}人在线" + }, + "typing": { + "simple": "正在输入...", + "user": "{user}正在输入...", + "group": { + "sequenced": "{users}正在输入...", + "many": "{count}个人正在输入..." + } + }, + "content": { + "unknown": "不支持的内容", + "document": "文档", + "photo": "照片", + "video": "图片", + "audio": "语音", + "contact": "通讯录", + "location": "位置", + "sticker": "贴纸", + "service": { + "registered": { + "full": "{name} 已加入 {app_name}", + "compact": "已加入 {app_name}" + }, + "groups": { + "created": "{name}创建了这个群组", + "invited": "{name}邀请{name_added}", + "joined": "{name}加入群组", + "kicked": "{name}移出{name_kicked}", + "left": "{name}退出群组", + "title_changed": { + "full": "{name}修改为名称为:\"{title}\"", + "compact": "{name}修改了群组名称" + }, + "topic_changed": { + "full": "{name} changed the group topic to \"{topic}\"", + "compact": "{name} changed the group topic" + }, + "about_changed": { + "full": "{name} changed the group about to \"{about}\"", + "compact": "{name} changed the group about" + }, + "avatar_changed": "{name}修改了群组头像", + "avatar_removed": "{name}删除了群组头像" + }, + "channels": { + "created": "{name}创建了这个群组", + "invited": "{name}邀请{name_added}", + "joined": "{name}加入群组", + "kicked": "{name}移出{name_kicked}", + "left": "{name}退出群组", + "title_changed": { + "full": "{name}修改为名称为:\"{title}\"", + "compact": "{name}修改了群组名称" + }, + "topic_changed": { + "full": "{name} changed the group topic to \"{topic}\"", + "compact": "{name} changed the group topic" + }, + "about_changed": { + "full": "{name} changed the group about to \"{about}\"", + "compact": "{name} changed the group about" + }, + "avatar_changed": "{name}修改了群组头像", + "avatar_removed": "{name}删除了群组头像" + }, + "calls": { + "missed": "未接电话", + "ended": "拨号结束" + }, + "encrypted": { + "timer_changed": { + "full": "{name} set self-destruct timer to {timer}", + "compact": "{name} set self-destruct timer" + }, + "timer_disabled": "{name} disabled self-destruct timer" + } + } + }, + "errors": { + "unknown": "Unknown error. Please, try again later.", + "internal": "服务器内部错误,请稍候再试。", + "phone": { + "empty": "请输入你的手机号码", + "incorrect": "输入的手机号码不正确,请重新输入。" + }, + "code": { + "empty": "请正确输入验证码。", + "incorrect": "输入的验证码不正确,请重试。", + "expired": "验证码过期,请重新验证。" + }, + "groups": { + "already_joined": "你已经是群组成员。", + "unable_to_join": "无法加入群组" + } + }, + "months": { + "january": "一月", + "february": "二月", + "march": "三月", + "april": "四月", + "may": "五月", + "june": "六月", + "july": "七月", + "august": "八月", + "september": "九月", + "october": "十月", + "november": "十一月", + "december": "十二月" + } } \ No newline at end of file diff --git a/actor-sdk/sdk-core/runtime/runtime-cocoa/src/main/java/im/actor/runtime/cocoa/CocoaCryptoProvider.java b/actor-sdk/sdk-core/runtime/runtime-cocoa/src/main/java/im/actor/runtime/cocoa/CocoaCryptoProvider.java index 3b81365845..f54510ec29 100644 --- a/actor-sdk/sdk-core/runtime/runtime-cocoa/src/main/java/im/actor/runtime/cocoa/CocoaCryptoProvider.java +++ b/actor-sdk/sdk-core/runtime/runtime-cocoa/src/main/java/im/actor/runtime/cocoa/CocoaCryptoProvider.java @@ -30,10 +30,10 @@ public Digest SHA256() { } @Override - public BlockCipher AES128(byte[] key) { + public BlockCipher AES256(byte[] key) { if (proxyProvider != null) { - return proxyProvider.createAES128(key); + return proxyProvider.createAES256(key); } - return super.AES128(key); + return super.AES256(key); } } diff --git a/actor-sdk/sdk-core/runtime/runtime-cocoa/src/main/java/im/actor/runtime/cocoa/crypto/CocoaCryptoProxyProvider.java b/actor-sdk/sdk-core/runtime/runtime-cocoa/src/main/java/im/actor/runtime/cocoa/crypto/CocoaCryptoProxyProvider.java index 144ddabcb3..e6a335d31a 100644 --- a/actor-sdk/sdk-core/runtime/runtime-cocoa/src/main/java/im/actor/runtime/cocoa/crypto/CocoaCryptoProxyProvider.java +++ b/actor-sdk/sdk-core/runtime/runtime-cocoa/src/main/java/im/actor/runtime/cocoa/crypto/CocoaCryptoProxyProvider.java @@ -10,6 +10,6 @@ public interface CocoaCryptoProxyProvider { @ObjectiveCName("createSHA256") Digest createSHA256(); - @ObjectiveCName("createAES128WithKey:") - BlockCipher createAES128(byte[] key); + @ObjectiveCName("createAES256WithKey:") + BlockCipher createAES256(byte[] key); } diff --git a/actor-sdk/sdk-core/runtime/runtime-generic/src/main/java/im/actor/runtime/binary/BinaryOp.java b/actor-sdk/sdk-core/runtime/runtime-generic/src/main/java/im/actor/runtime/binary/BinaryOp.java new file mode 100644 index 0000000000..8c3ca76aa1 --- /dev/null +++ b/actor-sdk/sdk-core/runtime/runtime-generic/src/main/java/im/actor/runtime/binary/BinaryOp.java @@ -0,0 +1,18 @@ +package im.actor.runtime.binary; + +public class BinaryOp { + + public static void intToBigEndian(int n, byte[] bs, int off) { + bs[off] = (byte) (n >>> 24); + bs[++off] = (byte) (n >>> 16); + bs[++off] = (byte) (n >>> 8); + bs[++off] = (byte) (n); + } + + public static void intToLittleEndian(int n, byte[] bs, int off) { + bs[off] = (byte) (n); + bs[++off] = (byte) (n >>> 8); + bs[++off] = (byte) (n >>> 16); + bs[++off] = (byte) (n >>> 24); + } +} diff --git a/actor-sdk/sdk-core/runtime/runtime-js/src/main/java/im/actor/runtime/binary/BinaryOp.java b/actor-sdk/sdk-core/runtime/runtime-js/src/main/java/im/actor/runtime/binary/BinaryOp.java new file mode 100644 index 0000000000..8aed903dbc --- /dev/null +++ b/actor-sdk/sdk-core/runtime/runtime-js/src/main/java/im/actor/runtime/binary/BinaryOp.java @@ -0,0 +1,26 @@ +package im.actor.runtime.binary; + +public class BinaryOp { + + public static void intToBigEndian(int n, byte[] bs, int off) { + bs[off] = jsWrap((byte) (n >>> 24)); + bs[++off] = jsWrap((byte) (n >>> 16)); + bs[++off] = jsWrap((byte) (n >>> 8)); + bs[++off] = jsWrap((byte) (n)); + } + + public static void intToLittleEndian(int n, byte[] bs, int off) { + bs[off] = jsWrap((byte) (n)); + bs[++off] = jsWrap((byte) (n >>> 8)); + bs[++off] = jsWrap((byte) (n >>> 16)); + bs[++off] = jsWrap((byte) (n >>> 24)); + } + + public static int jsWrap(int val) { + return val & 0xFFFFFFFF; + } + + public static byte jsWrap(byte val) { + return (byte) (val & 0xFF); + } +} diff --git a/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/Crypto.java b/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/Crypto.java index e80e9ed2c9..45b8424489 100644 --- a/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/Crypto.java +++ b/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/Crypto.java @@ -26,8 +26,8 @@ public static Digest createSHA256() { return runtime.SHA256(); } - public static BlockCipher createAES128(byte[] key) { - return runtime.AES128(key); + public static BlockCipher createAES256(byte[] key) { + return runtime.AES256(key); } public static byte[] MD5(byte[] data) { @@ -97,4 +97,20 @@ public static String hex(byte[] bytes) { public static byte[] fromHex(String hex) { return Hex.fromHex(hex); } + + /** + * Calculating padded length + * + * @param sourceLength source length + * @param blockSize block size + * @return padded length + */ + public static int paddedLength(int sourceLength, int blockSize) { + if (sourceLength % blockSize != 0) { + return sourceLength + (blockSize - sourceLength % blockSize); + } else { + return sourceLength; + } + + } } diff --git a/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/CryptoRuntime.java b/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/CryptoRuntime.java index d57e59b96d..462a91606d 100644 --- a/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/CryptoRuntime.java +++ b/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/CryptoRuntime.java @@ -14,7 +14,7 @@ public interface CryptoRuntime { Digest SHA256(); - BlockCipher AES128(byte[] key); + BlockCipher AES256(byte[] key); void waitForCryptoLoaded(); } \ No newline at end of file diff --git a/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/DefaultCryptoRuntime.java b/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/DefaultCryptoRuntime.java index 09c14514e3..ff4de7c672 100644 --- a/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/DefaultCryptoRuntime.java +++ b/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/DefaultCryptoRuntime.java @@ -13,7 +13,7 @@ public Digest SHA256() { } @Override - public BlockCipher AES128(byte[] key) { + public BlockCipher AES256(byte[] key) { return new AESFastEngine(key); } } diff --git a/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/actors/ActorSystem.java b/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/actors/ActorSystem.java index 8a491fee14..01cd75e926 100755 --- a/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/actors/ActorSystem.java +++ b/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/actors/ActorSystem.java @@ -139,15 +139,6 @@ public ActorRef actorOf(String path, String dispatcher, ActorCreator creator, Ac .changeSupervisor(supervisor), path); } - public ActorRef actorOf(String path, String dispatcher, final Constructor constructor) { - return actorOf(Props.create(new ActorCreator() { - @Override - public Actor create() { - return constructor.create(); - } - }).changeDispatcher(dispatcher), path); - } - /** * Getting current trace interface for actor system * diff --git a/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/crypto/box/ActorBox.java b/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/crypto/box/ActorBox.java index 84b44a2d7e..19e8c96b4f 100644 --- a/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/crypto/box/ActorBox.java +++ b/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/crypto/box/ActorBox.java @@ -3,7 +3,6 @@ import im.actor.runtime.Crypto; import im.actor.runtime.crypto.IntegrityException; -import im.actor.runtime.crypto.primitives.aes.AESFastEngine; import im.actor.runtime.crypto.primitives.kuznechik.KuznechikFastEngine; import im.actor.runtime.crypto.primitives.padding.PKCS7Padding; import im.actor.runtime.crypto.primitives.streebog.Streebog256; @@ -34,7 +33,7 @@ public class ActorBox { */ public static byte[] openBox(byte[] header, byte[] cipherText, ActorBoxKey key) throws IntegrityException { CBCHmacBox aesCipher = - new CBCHmacBox(Crypto.createAES128(key.getKeyAES()), Crypto.createSHA256(), key.getMacAES()); + new CBCHmacBox(Crypto.createAES256(key.getKeyAES()), Crypto.createSHA256(), key.getMacAES()); CBCHmacBox kuzCipher = new CBCHmacBox(new KuznechikFastEngine(key.getKeyKuz()), new Streebog256(), key.getMacKuz()); byte[] kuzPackage = aesCipher.decryptPackage(header, @@ -68,7 +67,7 @@ public static byte[] openBox(byte[] header, byte[] cipherText, ActorBoxKey key) * @throws IntegrityException */ public static byte[] closeBox(byte[] header, byte[] plainText, byte[] random32, ActorBoxKey key) throws IntegrityException { - CBCHmacBox aesCipher = new CBCHmacBox(Crypto.createAES128(key.getKeyAES()), Crypto.createSHA256(), key.getMacAES()); + CBCHmacBox aesCipher = new CBCHmacBox(Crypto.createAES256(key.getKeyAES()), Crypto.createSHA256(), key.getMacAES()); CBCHmacBox kuzCipher = new CBCHmacBox(new KuznechikFastEngine(key.getKeyKuz()), new Streebog256(), key.getMacKuz()); // Calculating padding diff --git a/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/crypto/primitives/modes/CBCBlockCipherStream.java b/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/crypto/primitives/modes/CBCBlockCipherStream.java new file mode 100644 index 0000000000..393b7928c5 --- /dev/null +++ b/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/crypto/primitives/modes/CBCBlockCipherStream.java @@ -0,0 +1,66 @@ +package im.actor.runtime.crypto.primitives.modes; + +import im.actor.runtime.crypto.BlockCipher; + +/*-[ +#define J2OBJC_DISABLE_ARRAY_BOUND_CHECKS 1 +]-*/ + +public class CBCBlockCipherStream implements BlockCipher { + + private byte[] iv; + private BlockCipher baseCipher; + private int blockSize; + private byte[] workingSet; + + public CBCBlockCipherStream(byte[] iv, BlockCipher baseCipher) { + if (iv.length != baseCipher.getBlockSize()) { + throw new RuntimeException("Incorrect iv size"); + } + + this.baseCipher = baseCipher; + this.blockSize = baseCipher.getBlockSize(); + this.iv = iv; + this.workingSet = new byte[blockSize]; + } + + @Override + public void encryptBlock(byte[] data, int offset, byte[] dest, int destOffset) { + +// // DATA = SRC_DATA ^ IV +// for (int j = 0; j < blockSize; j++) { +// workingSet[j] = (byte) ((data[offset + j] & 0xFF) ^ (iv[j] & 0xFF)); +// } + + // DEST = encrypt(DATA) + // baseCipher.encryptBlock(workingSet, 0, dest, destOffset); + baseCipher.encryptBlock(data, offset, dest, destOffset); + +// // IV = DEST +// for (int j = 0; j < blockSize; j++) { +// iv[j] = dest[destOffset + j]; +// } + } + + @Override + public void decryptBlock(byte[] data, int offset, byte[] dest, int destOffset) { + + // DEST = decrypt(DATA) + baseCipher.decryptBlock(data, offset, dest, destOffset); + +// // DEST_RES = DEST ^ IV +// for (int j = 0; j < blockSize; j++) { +// dest[destOffset + j] = (byte) ((dest[destOffset + j] & 0xFF) ^ (iv[j] & 0xFF)); +// } + +// // IV = DATA +// for (int j = 0; j < blockSize; j++) { +// iv[j] = data[offset + j]; +// } + } + + @Override + public int getBlockSize() { + return baseCipher.getBlockSize(); + } +} diff --git a/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/crypto/primitives/util/ByteStrings.java b/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/crypto/primitives/util/ByteStrings.java index 7a811ba57e..ecfe46a6c5 100644 --- a/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/crypto/primitives/util/ByteStrings.java +++ b/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/crypto/primitives/util/ByteStrings.java @@ -104,6 +104,15 @@ public static void write(byte[] dest, int destOffset, byte[] src, int srcOffset, } public static boolean isEquals(byte[] a, byte[] b) { + if (a == null && b == null) { + return true; + } + if (a != null && b == null) { + return false; + } + if (a == null) { + return false; + } if (a.length != b.length) { return false; } diff --git a/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/crypto/primitives/util/Pack.java b/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/crypto/primitives/util/Pack.java index 42f18ac972..50b050d324 100644 --- a/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/crypto/primitives/util/Pack.java +++ b/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/crypto/primitives/util/Pack.java @@ -2,6 +2,8 @@ import com.google.j2objc.annotations.AutoreleasePool; +import im.actor.runtime.binary.BinaryOp; + // Disabling Bounds checks for speeding up calculations /*-[ @@ -45,10 +47,7 @@ public static byte[] intToBigEndian(int n) { } public static void intToBigEndian(int n, byte[] bs, int off) { - bs[off] = jsWrap((byte) (n >>> 24)); - bs[++off] = jsWrap((byte) (n >>> 16)); - bs[++off] = jsWrap((byte) (n >>> 8)); - bs[++off] = jsWrap((byte) (n)); + BinaryOp.intToBigEndian(n, bs, off); } public static byte[] intToBigEndian(int[] ns) { @@ -137,10 +136,7 @@ public static byte[] intToLittleEndian(int n) { } public static void intToLittleEndian(int n, byte[] bs, int off) { - bs[off] = jsWrap((byte) (n)); - bs[++off] = jsWrap((byte) (n >>> 8)); - bs[++off] = jsWrap((byte) (n >>> 16)); - bs[++off] = jsWrap((byte) (n >>> 24)); + BinaryOp.intToLittleEndian(n, bs, off); } public static byte[] intToLittleEndian(int[] ns) { diff --git a/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/files/SequenceFileSystemInputFile.java b/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/files/SequenceFileSystemInputFile.java new file mode 100644 index 0000000000..e8174d76be --- /dev/null +++ b/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/files/SequenceFileSystemInputFile.java @@ -0,0 +1,32 @@ +package im.actor.runtime.files; + +import im.actor.runtime.actors.messages.Void; +import im.actor.runtime.promise.Promise; + +public class SequenceFileSystemInputFile implements SequenceInputFile { + + // j2objc workaround + private static final FilePart DUMB = null; + + private final InputFile inputFile; + private int currentOffset; + + private Promise prev = Promise.success(null); + + public SequenceFileSystemInputFile(InputFile inputFile) { + this.inputFile = inputFile; + } + + @Override + public synchronized Promise readBlock(int blockSize) { + int offset = currentOffset; + currentOffset += blockSize; + prev = prev.flatMap(r -> inputFile.read(offset, blockSize)); + return prev; + } + + @Override + public synchronized Promise close() { + return prev.flatMap(r -> inputFile.close()); + } +} \ No newline at end of file diff --git a/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/files/SequenceInputFile.java b/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/files/SequenceInputFile.java new file mode 100644 index 0000000000..4d237eef83 --- /dev/null +++ b/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/files/SequenceInputFile.java @@ -0,0 +1,11 @@ +package im.actor.runtime.files; + +import im.actor.runtime.actors.messages.Void; +import im.actor.runtime.promise.Promise; + +public interface SequenceInputFile { + + Promise readBlock(int blockSize); + + Promise close(); +} diff --git a/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/promise/Promises.java b/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/promise/Promises.java index c976bc840b..81af89f631 100644 --- a/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/promise/Promises.java +++ b/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/promise/Promises.java @@ -2,10 +2,15 @@ import com.google.j2objc.annotations.ObjectiveCName; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; import java.util.List; import im.actor.runtime.Log; +import im.actor.runtime.actors.messages.Void; import im.actor.runtime.function.Consumer; +import im.actor.runtime.function.Function; import im.actor.runtime.function.Supplier; import im.actor.runtime.function.Tuple2; import im.actor.runtime.function.Tuple3; @@ -16,6 +21,9 @@ */ public class Promises { + // j2objc workaround + private static final Void DUMB = null; + @ObjectiveCName("logWithTag:withResolver:withFunc:") public static Promise log(final String TAG, final PromiseResolver resolver, final PromiseFunc func) { return new Promise(r -> func.exec(r)).then(t -> { @@ -95,13 +103,104 @@ public static Promise> tuple(Promise * @param type of promises * @return promise */ - public static Promise traverse(List>> queue) { + public static Promise traverse(List>> queue) { if (queue.size() == 0) { return Promise.success(null); } return queue.remove(0).get() - .flatMap(v -> traverse(queue)); + .flatMap(v -> traverse(queue)) + .map(r -> null); + } + + /** + * Executing promises in parallel but return results consequently + * + * @param parallelFraction number of parallel promises + * @param next supplier of next promise + * @param consumer Consumer of results + * @param type of promises + */ + public static Promise traverseParallel(int parallelFraction, Supplier> next, Consumer consumer) { + return new Promise<>((PromiseFunc) resolver -> { + TraverseHolder state = new TraverseHolder<>(next, consumer, resolver); + for (int i = 0; i < parallelFraction; i++) { + state.spawn(); + } + }); + } + + private static class TraverseHolder { + + public Supplier> next; + public Consumer consumer; + public Promise latestPromise; + public PromiseResolver resolver; + public boolean isEnded; + public boolean isFetchingEnded; + + public TraverseHolder(Supplier> next, Consumer consumer, PromiseResolver resolver) { + this.next = next; + this.consumer = consumer; + this.resolver = resolver; + } + + + public void spawn() { + if (isFetchingEnded || isEnded) { + return; + } + Promise p = next.get(); + if (p == null) { + isFetchingEnded = true; + if (latestPromise == null) { + isEnded = true; + resolver.result(null); + } else { + latestPromise.then(t -> { + if (!isEnded) { + isEnded = true; + } + resolver.result(null); + }); + } + return; + } + + Promise chained; + if (latestPromise == null) { + chained = p; + } else { + Promise forChain = latestPromise; + chained = p.chain(r -> forChain); + } + latestPromise = chained; + + + chained.then(t -> { + if (isEnded) { + return; + } + + try { + consumer.apply(t); + } catch (Exception e) { + isEnded = true; + resolver.error(e); + return; + } + + spawn(); + }); + + chained.failure(e -> { + if (isEnded) { + return; + } + isEnded = true; + resolver.error(e); + }); + } } } \ No newline at end of file diff --git a/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/promise/PromisesArray.java b/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/promise/PromisesArray.java index 3eda9ce2cb..78f1f6fc93 100644 --- a/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/promise/PromisesArray.java +++ b/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/promise/PromisesArray.java @@ -130,6 +130,11 @@ public PromisesArray ignoreFailed() { })); } + public PromisesArray filterFailed() { + return ignoreFailed() + .filterNull(); + } + public PromisesArray filterNull() { return filter(Predicates.NOT_NULL); } @@ -364,4 +369,13 @@ public Promise zipPromise(final ListFunction> fuc) { public Promise> zip() { return zipPromise(t -> Promise.success(t)); } + + /** + * Zipping array of promises to single promise of array + * + * @return promise + */ + public Promise zip(Function, V> mapfunc) { + return zip().map(mapfunc); + } } \ No newline at end of file diff --git a/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/util/Hex.java b/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/util/Hex.java index 391bb160b8..5506ccb3df 100644 --- a/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/util/Hex.java +++ b/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/util/Hex.java @@ -49,4 +49,12 @@ public static byte[] fromHex(String hex) { } return res; } + + public static byte[] fromHexReverse(String hex) { + byte[] res = new byte[hex.length() / 2]; + for (int i = 0; i < res.length; i++) { + res[res.length - i - 1] = (byte) ((fromHexShort(hex.charAt(i * 2)) << 4) + fromHexShort(hex.charAt(i * 2 + 1))); + } + return res; + } } diff --git a/actor-sdk/sdk-core/runtime/runtime-shared/src/template/java/im/actor/runtime/binary/BinaryOp.java b/actor-sdk/sdk-core/runtime/runtime-shared/src/template/java/im/actor/runtime/binary/BinaryOp.java new file mode 100644 index 0000000000..32a9351909 --- /dev/null +++ b/actor-sdk/sdk-core/runtime/runtime-shared/src/template/java/im/actor/runtime/binary/BinaryOp.java @@ -0,0 +1,12 @@ +package im.actor.runtime.binary; + +public class BinaryOp { + + public static void intToBigEndian(int n, byte[] bs, int off) { + throw new RuntimeException("DUMB"); + } + + public static void intToLittleEndian(int n, byte[] bs, int off) { + throw new RuntimeException("DUMB"); + } +}