mirror of
https://github.com/oxen-io/session-android.git
synced 2025-08-11 20:47:42 +00:00
Merge branch 'dev' into pr/1026
This commit is contained in:
4
libsession/src/debug/res/values/values.xml
Normal file
4
libsession/src/debug/res/values/values.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<bool name="screen_security_default">false</bool>
|
||||
</resources>
|
@@ -8,9 +8,11 @@ import androidx.annotation.Nullable;
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.session.libsession.utilities.Address;
|
||||
import org.session.libsignal.utilities.Log;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
@@ -22,9 +24,9 @@ public class AvatarHelper {
|
||||
private static final String AVATAR_DIRECTORY = "avatars";
|
||||
|
||||
public static InputStream getInputStreamFor(@NonNull Context context, @NonNull Address address)
|
||||
throws IOException
|
||||
throws FileNotFoundException
|
||||
{
|
||||
return new FileInputStream(getAvatarFile(context, address));
|
||||
return new FileInputStream(getAvatarFile(context, address));
|
||||
}
|
||||
|
||||
public static List<File> getAvatarFiles(@NonNull Context context) {
|
||||
|
@@ -1,13 +1,10 @@
|
||||
package org.session.libsession.avatars
|
||||
|
||||
import android.content.Context
|
||||
import com.bumptech.glide.load.Key
|
||||
import java.security.MessageDigest
|
||||
|
||||
class PlaceholderAvatarPhoto(val context: Context,
|
||||
val hashString: String,
|
||||
class PlaceholderAvatarPhoto(val hashString: String,
|
||||
val displayName: String): Key {
|
||||
|
||||
override fun updateDiskCacheKey(messageDigest: MessageDigest) {
|
||||
messageDigest.update(hashString.encodeToByteArray())
|
||||
messageDigest.update(displayName.encodeToByteArray())
|
||||
|
@@ -8,6 +8,7 @@ import androidx.annotation.Nullable;
|
||||
|
||||
import org.session.libsession.utilities.Address;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.MessageDigest;
|
||||
@@ -24,7 +25,7 @@ public class ProfileContactPhoto implements ContactPhoto {
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream openInputStream(Context context) throws IOException {
|
||||
public InputStream openInputStream(Context context) throws FileNotFoundException {
|
||||
return AvatarHelper.getInputStreamFor(context, address);
|
||||
}
|
||||
|
||||
|
@@ -8,11 +8,11 @@ import android.graphics.drawable.LayerDrawable;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.appcompat.content.res.AppCompatResources;
|
||||
|
||||
import com.amulyakhare.textdrawable.TextDrawable;
|
||||
import com.makeramen.roundedimageview.RoundedDrawable;
|
||||
|
||||
|
||||
import org.session.libsession.R;
|
||||
import org.session.libsession.utilities.ThemeUtil;
|
||||
|
||||
@@ -32,16 +32,18 @@ public class ResourceContactPhoto implements FallbackContactPhoto {
|
||||
@Override
|
||||
public Drawable asDrawable(Context context, int color, boolean inverted) {
|
||||
Drawable background = TextDrawable.builder().buildRound(" ", inverted ? Color.WHITE : color);
|
||||
RoundedDrawable foreground = (RoundedDrawable) RoundedDrawable.fromDrawable(context.getResources().getDrawable(resourceId));
|
||||
RoundedDrawable foreground = (RoundedDrawable) RoundedDrawable.fromDrawable(AppCompatResources.getDrawable(context, resourceId));
|
||||
|
||||
foreground.setScaleType(ImageView.ScaleType.CENTER);
|
||||
foreground.setScaleType(ImageView.ScaleType.CENTER_CROP);
|
||||
|
||||
if (inverted) {
|
||||
foreground.setColorFilter(color, PorterDuff.Mode.SRC_ATOP);
|
||||
}
|
||||
|
||||
Drawable gradient = context.getResources().getDrawable(ThemeUtil.isDarkTheme(context) ? R.drawable.avatar_gradient_dark
|
||||
: R.drawable.avatar_gradient_light);
|
||||
Drawable gradient = AppCompatResources.getDrawable(
|
||||
context,
|
||||
ThemeUtil.isDarkTheme(context) ? R.drawable.avatar_gradient_dark : R.drawable.avatar_gradient_light
|
||||
);
|
||||
|
||||
return new ExpandingLayerDrawable(new Drawable[] {background, foreground, gradient});
|
||||
}
|
||||
|
@@ -20,9 +20,11 @@ interface MessageDataProvider {
|
||||
* @return pair of sms or mms table-specific ID and whether it is in SMS table
|
||||
*/
|
||||
fun getMessageID(serverId: Long, threadId: Long): Pair<Long, Boolean>?
|
||||
fun getMessageIDs(serverIDs: List<Long>, threadID: Long): Pair<List<Long>, List<Long>>
|
||||
fun deleteMessage(messageID: Long, isSms: Boolean)
|
||||
fun updateMessageAsDeleted(timestamp: Long, author: String)
|
||||
fun getServerHashForMessage(messageID: Long): String?
|
||||
fun deleteMessages(messageIDs: List<Long>, threadId: Long, isSms: Boolean)
|
||||
fun updateMessageAsDeleted(timestamp: Long, author: String): Long?
|
||||
fun getServerHashForMessage(messageID: Long, mms: Boolean): String?
|
||||
fun getDatabaseAttachment(attachmentId: Long): DatabaseAttachment?
|
||||
fun getAttachmentStream(attachmentId: Long): SessionServiceAttachmentStream?
|
||||
fun getAttachmentPointer(attachmentId: Long): SessionServiceAttachmentPointer?
|
||||
@@ -36,7 +38,7 @@ interface MessageDataProvider {
|
||||
fun isOutgoingMessage(timestamp: Long): Boolean
|
||||
fun handleSuccessfulAttachmentUpload(attachmentId: Long, attachmentStream: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: UploadResult)
|
||||
fun handleFailedAttachmentUpload(attachmentId: Long)
|
||||
fun getMessageForQuote(timestamp: Long, author: Address): Pair<Long, Boolean>?
|
||||
fun getMessageForQuote(timestamp: Long, author: Address): Triple<Long, Boolean, String>?
|
||||
fun getAttachmentsAndLinkPreviewFor(mmsId: Long): List<Attachment>
|
||||
fun getMessageBodyFor(timestamp: Long, author: String): String
|
||||
fun getAttachmentIDsFor(messageID: Long): List<Long>
|
||||
|
@@ -2,20 +2,25 @@ package org.session.libsession.database
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import network.loki.messenger.libsession_util.ConfigBase
|
||||
import org.session.libsession.messaging.BlindedIdMapping
|
||||
import org.session.libsession.messaging.calls.CallMessageType
|
||||
import org.session.libsession.messaging.contacts.Contact
|
||||
import org.session.libsession.messaging.jobs.AttachmentUploadJob
|
||||
import org.session.libsession.messaging.jobs.Job
|
||||
import org.session.libsession.messaging.jobs.MessageSendJob
|
||||
import org.session.libsession.messaging.messages.Destination
|
||||
import org.session.libsession.messaging.messages.ExpirationConfiguration
|
||||
import org.session.libsession.messaging.messages.Message
|
||||
import org.session.libsession.messaging.messages.control.ConfigurationMessage
|
||||
import org.session.libsession.messaging.messages.control.MessageRequestResponse
|
||||
import org.session.libsession.messaging.messages.visible.Attachment
|
||||
import org.session.libsession.messaging.messages.visible.Profile
|
||||
import org.session.libsession.messaging.messages.visible.Reaction
|
||||
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
||||
import org.session.libsession.messaging.open_groups.GroupMember
|
||||
import org.session.libsession.messaging.open_groups.OpenGroup
|
||||
import org.session.libsession.messaging.open_groups.OpenGroupApi
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
|
||||
import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage
|
||||
@@ -28,16 +33,19 @@ import org.session.libsession.utilities.recipients.Recipient.RecipientSettings
|
||||
import org.session.libsignal.crypto.ecc.ECKeyPair
|
||||
import org.session.libsignal.messages.SignalServiceAttachmentPointer
|
||||
import org.session.libsignal.messages.SignalServiceGroup
|
||||
import network.loki.messenger.libsession_util.util.Contact as LibSessionContact
|
||||
|
||||
interface StorageProtocol {
|
||||
|
||||
// General
|
||||
fun getUserPublicKey(): String?
|
||||
fun getUserX25519KeyPair(): ECKeyPair
|
||||
fun getUserDisplayName(): String?
|
||||
fun getUserProfileKey(): ByteArray?
|
||||
fun getUserProfilePictureURL(): String?
|
||||
fun setUserProfilePictureURL(newProfilePicture: String)
|
||||
fun getUserProfile(): Profile
|
||||
fun setProfileAvatar(recipient: Recipient, profileAvatar: String?)
|
||||
fun setProfilePicture(recipient: Recipient, newProfilePicture: String?, newProfileKey: ByteArray?)
|
||||
fun setBlocksCommunityMessageRequests(recipient: Recipient, blocksMessageRequests: Boolean)
|
||||
fun setUserProfilePicture(newProfilePicture: String?, newProfileKey: ByteArray?)
|
||||
fun clearUserPic()
|
||||
// Signal
|
||||
fun getOrGenerateRegistrationID(): Int
|
||||
|
||||
@@ -49,9 +57,11 @@ interface StorageProtocol {
|
||||
fun getAttachmentUploadJob(attachmentID: Long): AttachmentUploadJob?
|
||||
fun getMessageSendJob(messageSendJobID: String): MessageSendJob?
|
||||
fun getMessageReceiveJob(messageReceiveJobID: String): Job?
|
||||
fun getGroupAvatarDownloadJob(server: String, room: String): Job?
|
||||
fun getGroupAvatarDownloadJob(server: String, room: String, imageId: String?): Job?
|
||||
fun getConfigSyncJob(destination: Destination): Job?
|
||||
fun resumeMessageSendJobIfNeeded(messageSendJobID: String)
|
||||
fun isJobCanceled(job: Job): Boolean
|
||||
fun cancelPendingMessageSendJobs(threadID: Long)
|
||||
|
||||
// Authorization
|
||||
fun getAuthToken(room: String, server: String): String?
|
||||
@@ -66,8 +76,8 @@ interface StorageProtocol {
|
||||
fun getAllOpenGroups(): Map<Long, OpenGroup>
|
||||
fun updateOpenGroup(openGroup: OpenGroup)
|
||||
fun getOpenGroup(threadId: Long): OpenGroup?
|
||||
fun addOpenGroup(urlAsString: String)
|
||||
fun onOpenGroupAdded(server: String)
|
||||
fun addOpenGroup(urlAsString: String): OpenGroupApi.RoomInfo?
|
||||
fun onOpenGroupAdded(server: String, room: String)
|
||||
fun hasBackgroundGroupAddJob(groupJoinUrl: String): Boolean
|
||||
fun setOpenGroupServerMessageID(messageID: Long, serverID: Long, threadID: Long, isSms: Boolean)
|
||||
fun getOpenGroup(room: String, server: String): OpenGroup?
|
||||
@@ -80,6 +90,8 @@ interface StorageProtocol {
|
||||
// Open Group Metadata
|
||||
fun updateTitle(groupID: String, newValue: String)
|
||||
fun updateProfilePicture(groupID: String, newValue: ByteArray)
|
||||
fun removeProfilePicture(groupID: String)
|
||||
fun hasDownloadedProfilePicture(groupID: String): Boolean
|
||||
fun setUserCount(room: String, server: String, newValue: Int)
|
||||
|
||||
// Last Message Server ID
|
||||
@@ -102,17 +114,25 @@ interface StorageProtocol {
|
||||
*/
|
||||
fun persistAttachments(messageID: Long, attachments: List<Attachment>): List<Long>
|
||||
fun getAttachmentsForMessage(messageID: Long): List<DatabaseAttachment>
|
||||
fun getMessageIdInDatabase(timestamp: Long, author: String): Long? // TODO: This is a weird name
|
||||
fun getMessageIdInDatabase(timestamp: Long, author: String): Pair<Long, Boolean>? // TODO: This is a weird name
|
||||
fun updateSentTimestamp(messageID: Long, isMms: Boolean, openGroupSentTimestamp: Long, threadId: Long)
|
||||
fun markAsResyncing(timestamp: Long, author: String)
|
||||
fun markAsSyncing(timestamp: Long, author: String)
|
||||
fun markAsSending(timestamp: Long, author: String)
|
||||
fun markAsSent(timestamp: Long, author: String)
|
||||
fun markAsSentToCommunity(threadID: Long, messageID: Long)
|
||||
fun markUnidentified(timestamp: Long, author: String)
|
||||
fun setErrorMessage(timestamp: Long, author: String, error: Exception)
|
||||
fun setMessageServerHash(messageID: Long, serverHash: String)
|
||||
fun markUnidentifiedInCommunity(threadID: Long, messageID: Long)
|
||||
fun markAsSyncFailed(timestamp: Long, author: String, error: Exception)
|
||||
fun markAsSentFailed(timestamp: Long, author: String, error: Exception)
|
||||
fun clearErrorMessage(messageID: Long)
|
||||
fun setMessageServerHash(messageID: Long, mms: Boolean, serverHash: String)
|
||||
|
||||
// Closed Groups
|
||||
fun getGroup(groupID: String): GroupRecord?
|
||||
fun createGroup(groupID: String, title: String?, members: List<Address>, avatar: SignalServiceAttachmentPointer?, relay: String?, admins: List<Address>, formationTimestamp: Long)
|
||||
fun createInitialConfigGroup(groupPublicKey: String, name: String, members: Map<String, Boolean>, formationTimestamp: Long, encryptionKeyPair: ECKeyPair, expirationTimer: Int)
|
||||
fun updateGroupConfig(groupPublicKey: String)
|
||||
fun isGroupActive(groupPublicKey: String): Boolean
|
||||
fun setActive(groupID: String, value: Boolean)
|
||||
fun getZombieMembers(groupID: String): Set<String>
|
||||
@@ -123,7 +143,7 @@ interface StorageProtocol {
|
||||
fun getAllActiveClosedGroupPublicKeys(): Set<String>
|
||||
fun addClosedGroupPublicKey(groupPublicKey: String)
|
||||
fun removeClosedGroupPublicKey(groupPublicKey: String)
|
||||
fun addClosedGroupEncryptionKeyPair(encryptionKeyPair: ECKeyPair, groupPublicKey: String)
|
||||
fun addClosedGroupEncryptionKeyPair(encryptionKeyPair: ECKeyPair, groupPublicKey: String, timestamp: Long)
|
||||
fun removeAllClosedGroupEncryptionKeyPairs(groupPublicKey: String)
|
||||
fun insertIncomingInfoMessage(context: Context, senderPublicKey: String, groupID: String, type: SignalServiceGroup.Type,
|
||||
name: String, members: Collection<String>, admins: Collection<String>, sentTimestamp: Long)
|
||||
@@ -134,18 +154,18 @@ interface StorageProtocol {
|
||||
fun getLatestClosedGroupEncryptionKeyPair(groupPublicKey: String): ECKeyPair?
|
||||
fun updateFormationTimestamp(groupID: String, formationTimestamp: Long)
|
||||
fun updateTimestampUpdated(groupID: String, updatedTimestamp: Long)
|
||||
fun setExpirationTimer(groupID: String, duration: Int)
|
||||
|
||||
// Groups
|
||||
fun getAllGroups(): List<GroupRecord>
|
||||
fun getAllGroups(includeInactive: Boolean): List<GroupRecord>
|
||||
|
||||
// Settings
|
||||
fun setProfileSharing(address: Address, value: Boolean)
|
||||
|
||||
// Thread
|
||||
fun getOrCreateThreadIdFor(address: Address): Long
|
||||
fun getOrCreateThreadIdFor(publicKey: String, groupPublicKey: String?, openGroupID: String?): Long
|
||||
fun getThreadIdFor(publicKey: String, groupPublicKey: String?, openGroupID: String?, createThread: Boolean): Long?
|
||||
fun getThreadId(publicKeyOrOpenGroupID: String): Long?
|
||||
fun getThreadId(openGroup: OpenGroup): Long?
|
||||
fun getThreadId(address: Address): Long?
|
||||
fun getThreadId(recipient: Recipient): Long?
|
||||
fun getThreadIdForMms(mmsId: Long): Long
|
||||
@@ -153,6 +173,12 @@ interface StorageProtocol {
|
||||
fun trimThread(threadID: Long, threadLimit: Int)
|
||||
fun trimThreadBefore(threadID: Long, timestamp: Long)
|
||||
fun getMessageCount(threadID: Long): Long
|
||||
fun setPinned(threadID: Long, isPinned: Boolean)
|
||||
fun isPinned(threadID: Long): Boolean
|
||||
fun deleteConversation(threadID: Long)
|
||||
fun setThreadDate(threadId: Long, newDate: Long)
|
||||
fun getLastLegacyRecipient(threadRecipient: String): String?
|
||||
fun setLastLegacyRecipient(threadRecipient: String, senderRecipient: String?)
|
||||
|
||||
// Contacts
|
||||
fun getContactWithSessionID(sessionID: String): Contact?
|
||||
@@ -160,6 +186,7 @@ interface StorageProtocol {
|
||||
fun setContact(contact: Contact)
|
||||
fun getRecipientForThread(threadId: Long): Recipient?
|
||||
fun getRecipientSettings(address: Address): RecipientSettings?
|
||||
fun addLibSessionContacts(contacts: List<LibSessionContact>, timestamp: Long)
|
||||
fun addContacts(contacts: List<ConfigurationMessage.Contact>)
|
||||
|
||||
// Attachments
|
||||
@@ -170,13 +197,14 @@ interface StorageProtocol {
|
||||
/**
|
||||
* Returns the ID of the `TSIncomingMessage` that was constructed.
|
||||
*/
|
||||
fun persist(message: VisibleMessage, quotes: QuoteModel?, linkPreview: List<LinkPreview?>, groupPublicKey: String?, openGroupID: String?, attachments: List<Attachment>, runIncrement: Boolean, runThreadUpdate: Boolean): Long?
|
||||
fun markConversationAsRead(threadId: Long, updateLastSeen: Boolean)
|
||||
fun incrementUnread(threadId: Long, amount: Int)
|
||||
fun persist(message: VisibleMessage, quotes: QuoteModel?, linkPreview: List<LinkPreview?>, groupPublicKey: String?, openGroupID: String?, attachments: List<Attachment>, runThreadUpdate: Boolean): Long?
|
||||
fun markConversationAsRead(threadId: Long, lastSeenTime: Long, force: Boolean = false)
|
||||
fun getLastSeen(threadId: Long): Long
|
||||
fun updateThread(threadId: Long, unarchive: Boolean)
|
||||
fun insertDataExtractionNotificationMessage(senderPublicKey: String, message: DataExtractionNotificationInfoMessage, sentTimestamp: Long)
|
||||
fun insertMessageRequestResponse(response: MessageRequestResponse)
|
||||
fun setRecipientApproved(recipient: Recipient, approved: Boolean)
|
||||
fun getRecipientApproved(address: Address): Boolean
|
||||
fun setRecipientApprovedMe(recipient: Recipient, approvedMe: Boolean)
|
||||
fun insertCallMessage(senderPublicKey: String, callMessageType: CallMessageType, sentTimestamp: Long)
|
||||
fun conversationHasOutgoing(userPublicKey: String): Boolean
|
||||
@@ -196,6 +224,21 @@ interface StorageProtocol {
|
||||
fun removeReaction(emoji: String, messageTimestamp: Long, author: String, notifyUnread: Boolean)
|
||||
fun updateReactionIfNeeded(message: Message, sender: String, openGroupSentTimestamp: Long)
|
||||
fun deleteReactions(messageId: Long, mms: Boolean)
|
||||
fun unblock(toUnblock: List<Recipient>)
|
||||
fun setBlocked(recipients: Iterable<Recipient>, isBlocked: Boolean, fromConfigUpdate: Boolean = false)
|
||||
fun setRecipientHash(recipient: Recipient, recipientHash: String?)
|
||||
fun blockedContacts(): List<Recipient>
|
||||
fun getExpirationConfiguration(threadId: Long): ExpirationConfiguration?
|
||||
fun setExpirationConfiguration(config: ExpirationConfiguration)
|
||||
fun getExpiringMessages(messageIds: List<Long> = emptyList()): List<Pair<Long, Long>>
|
||||
fun updateDisappearingState(
|
||||
messageSender: String,
|
||||
threadID: Long,
|
||||
disappearingState: Recipient.DisappearingState
|
||||
)
|
||||
|
||||
// Shared configs
|
||||
fun notifyConfigUpdates(forConfigObject: ConfigBase, messageTimestamp: Long)
|
||||
fun conversationInConfig(publicKey: String?, groupPublicKey: String?, openGroupId: String?, visibleOnly: Boolean): Boolean
|
||||
fun canPerformConfigChange(variant: String, publicKey: String, changeTimestampMs: Long): Boolean
|
||||
fun isCheckingCommunityRequests(): Boolean
|
||||
}
|
||||
|
@@ -0,0 +1,9 @@
|
||||
package org.session.libsession.messaging
|
||||
|
||||
interface LastSentTimestampCache {
|
||||
fun getTimestamp(threadId: Long): Long?
|
||||
fun submitTimestamp(threadId: Long, timestamp: Long)
|
||||
fun delete(threadId: Long, timestamps: List<Long>)
|
||||
fun delete(threadId: Long, timestamp: Long) = delete(threadId, listOf(timestamp))
|
||||
fun refresh(threadId: Long)
|
||||
}
|
@@ -4,12 +4,17 @@ import android.content.Context
|
||||
import com.goterl.lazysodium.utils.KeyPair
|
||||
import org.session.libsession.database.MessageDataProvider
|
||||
import org.session.libsession.database.StorageProtocol
|
||||
import org.session.libsession.utilities.ConfigFactoryProtocol
|
||||
import org.session.libsession.utilities.Device
|
||||
|
||||
class MessagingModuleConfiguration(
|
||||
val context: Context,
|
||||
val storage: StorageProtocol,
|
||||
val device: Device,
|
||||
val messageDataProvider: MessageDataProvider,
|
||||
val getUserED25519KeyPair: ()-> KeyPair?
|
||||
val getUserED25519KeyPair: () -> KeyPair?,
|
||||
val configFactory: ConfigFactoryProtocol,
|
||||
val lastSentTimestampCache: LastSentTimestampCache
|
||||
) {
|
||||
|
||||
companion object {
|
||||
|
@@ -77,7 +77,7 @@ class Contact(val sessionID: String) {
|
||||
companion object {
|
||||
|
||||
fun contextForRecipient(recipient: Recipient): ContactContext {
|
||||
return if (recipient.isOpenGroupRecipient) ContactContext.OPEN_GROUP else ContactContext.REGULAR
|
||||
return if (recipient.isCommunityRecipient) ContactContext.OPEN_GROUP else ContactContext.REGULAR
|
||||
}
|
||||
}
|
||||
}
|
@@ -16,15 +16,6 @@ object FileServerApi {
|
||||
private const val serverPublicKey = "da21e1d886c6fbaea313f75298bd64aab03a97ce985b46bb2dad9f2089c8ee59"
|
||||
const val server = "http://filev2.getsession.org"
|
||||
const val maxFileSize = 10_000_000 // 10 MB
|
||||
/**
|
||||
* The file server has a file size limit of `maxFileSize`, which the Service Nodes try to enforce as well. However, the limit applied by the Service Nodes
|
||||
* is on the **HTTP request** and not the actual file size. Because the file server expects the file data to be base 64 encoded, the size of the HTTP
|
||||
* request for a given file will be at least `ceil(n / 3) * 4` bytes, where n is the file size in bytes. This is the minimum size because there might also
|
||||
* be other parameters in the request. On average the multiplier appears to be about 1.5, so when checking whether the file will exceed the file size limit when
|
||||
* uploading a file we just divide the size of the file by this number. The alternative would be to actually check the size of the HTTP request but that's only
|
||||
* possible after proof of work has been calculated and the onion request encryption has happened, which takes several seconds.
|
||||
*/
|
||||
const val fileSizeORMultiplier = 2 // TODO: It should be possible to set this to 1.5?
|
||||
|
||||
sealed class Error(message: String) : Exception(message) {
|
||||
object ParsingFailed : Error("Invalid response.")
|
||||
@@ -77,7 +68,11 @@ object FileServerApi {
|
||||
OnionRequestAPI.sendOnionRequest(requestBuilder.build(), server, serverPublicKey).map {
|
||||
it.body ?: throw Error.ParsingFailed
|
||||
}.fail { e ->
|
||||
Log.e("Loki", "File server request failed.", e)
|
||||
when (e) {
|
||||
// No need for the stack trace for HTTP errors
|
||||
is HTTP.HTTPRequestFailedException -> Log.e("Loki", "File server request failed due to error: ${e.message}")
|
||||
else -> Log.e("Loki", "File server request failed", e)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Promise.ofFail(IllegalStateException("It's currently not allowed to send non onion routed requests."))
|
||||
@@ -96,7 +91,10 @@ object FileServerApi {
|
||||
)
|
||||
return send(request).map { response ->
|
||||
val json = JsonUtil.fromJson(response, Map::class.java)
|
||||
(json["id"] as? String)?.toLong() ?: throw Error.ParsingFailed
|
||||
val hasId = json.containsKey("id")
|
||||
val id = json.getOrDefault("id", null)
|
||||
Log.d("Loki-FS", "File Upload Response hasId: $hasId of type: ${id?.javaClass}")
|
||||
(id as? String)?.toLong() ?: throw Error.ParsingFailed
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -35,14 +35,14 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long)
|
||||
override val maxFailureCount: Int = 2
|
||||
|
||||
companion object {
|
||||
val KEY: String = "AttachmentDownloadJob"
|
||||
const val KEY: String = "AttachmentDownloadJob"
|
||||
|
||||
// Keys used for database storage
|
||||
private val ATTACHMENT_ID_KEY = "attachment_id"
|
||||
private val TS_INCOMING_MESSAGE_ID_KEY = "tsIncoming_message_id"
|
||||
}
|
||||
|
||||
override fun execute() {
|
||||
override suspend fun execute(dispatcherName: String) {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
|
||||
val threadID = storage.getThreadIdForMms(databaseMessageID)
|
||||
@@ -59,7 +59,7 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long)
|
||||
Log.d("AttachmentDownloadJob", "Setting attachment state = failed, don't have attachment")
|
||||
messageDataProvider.setAttachmentState(AttachmentState.FAILED, AttachmentId(attachmentID,0), databaseMessageID)
|
||||
}
|
||||
this.handlePermanentFailure(exception)
|
||||
this.handlePermanentFailure(dispatcherName, exception)
|
||||
} else if (exception == Error.DuplicateData) {
|
||||
attachment?.let { id ->
|
||||
Log.d("AttachmentDownloadJob", "Setting attachment state = done from duplicate data")
|
||||
@@ -68,7 +68,7 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long)
|
||||
Log.d("AttachmentDownloadJob", "Setting attachment state = done from duplicate data")
|
||||
messageDataProvider.setAttachmentState(AttachmentState.DONE, AttachmentId(attachmentID,0), databaseMessageID)
|
||||
}
|
||||
this.handleSuccess()
|
||||
this.handleSuccess(dispatcherName)
|
||||
} else {
|
||||
if (failureCount + 1 >= maxFailureCount) {
|
||||
attachment?.let { id ->
|
||||
@@ -79,7 +79,7 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long)
|
||||
messageDataProvider.setAttachmentState(AttachmentState.FAILED, AttachmentId(attachmentID,0), databaseMessageID)
|
||||
}
|
||||
}
|
||||
this.handleFailure(exception)
|
||||
this.handleFailure(dispatcherName, exception)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,19 +89,21 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long)
|
||||
}
|
||||
|
||||
val threadRecipient = storage.getRecipientForThread(threadID)
|
||||
val sender = if (messageDataProvider.isMmsOutgoing(databaseMessageID)) {
|
||||
val selfSend = messageDataProvider.isMmsOutgoing(databaseMessageID)
|
||||
val sender = if (selfSend) {
|
||||
storage.getUserPublicKey()
|
||||
} else {
|
||||
messageDataProvider.getIndividualRecipientForMms(databaseMessageID)?.address?.serialize()
|
||||
}
|
||||
val contact = sender?.let { storage.getContactWithSessionID(it) }
|
||||
if (threadRecipient == null || sender == null || contact == null) {
|
||||
if (threadRecipient == null || sender == null || (contact == null && !selfSend)) {
|
||||
handleFailure(Error.NoSender, null)
|
||||
return
|
||||
}
|
||||
if (!threadRecipient.isGroupRecipient && (!contact.isTrusted && storage.getUserPublicKey() != sender)) {
|
||||
if (!threadRecipient.isGroupRecipient && contact?.isTrusted != true && storage.getUserPublicKey() != sender) {
|
||||
// if we aren't receiving a group message, a message from ourselves (self-send) and the contact sending is not trusted:
|
||||
// do not continue, but do not fail
|
||||
handleFailure(Error.NoSender, null)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -150,7 +152,7 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long)
|
||||
Log.d("AttachmentDownloadJob", "deleting tempfile")
|
||||
tempFile.delete()
|
||||
Log.d("AttachmentDownloadJob", "succeeding job")
|
||||
handleSuccess()
|
||||
handleSuccess(dispatcherName)
|
||||
} catch (e: Exception) {
|
||||
Log.e("AttachmentDownloadJob", "Error processing attachment download", e)
|
||||
tempFile?.delete()
|
||||
@@ -169,17 +171,17 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSuccess() {
|
||||
private fun handleSuccess(dispatcherName: String) {
|
||||
Log.w("AttachmentDownloadJob", "Attachment downloaded successfully.")
|
||||
delegate?.handleJobSucceeded(this)
|
||||
delegate?.handleJobSucceeded(this, dispatcherName)
|
||||
}
|
||||
|
||||
private fun handlePermanentFailure(e: Exception) {
|
||||
delegate?.handleJobFailedPermanently(this, e)
|
||||
private fun handlePermanentFailure(dispatcherName: String, e: Exception) {
|
||||
delegate?.handleJobFailedPermanently(this, dispatcherName, e)
|
||||
}
|
||||
|
||||
private fun handleFailure(e: Exception) {
|
||||
delegate?.handleJobFailed(this, e)
|
||||
private fun handleFailure(dispatcherName: String, e: Exception) {
|
||||
delegate?.handleJobFailed(this, dispatcherName, e)
|
||||
}
|
||||
|
||||
private fun createTempFile(): File {
|
||||
|
@@ -16,7 +16,11 @@ import org.session.libsession.utilities.DecodedAudio
|
||||
import org.session.libsession.utilities.InputStreamMediaDataSource
|
||||
import org.session.libsession.utilities.UploadResult
|
||||
import org.session.libsignal.messages.SignalServiceAttachmentStream
|
||||
import org.session.libsignal.streams.*
|
||||
import org.session.libsignal.streams.AttachmentCipherOutputStream
|
||||
import org.session.libsignal.streams.AttachmentCipherOutputStreamFactory
|
||||
import org.session.libsignal.streams.DigestingRequestBody
|
||||
import org.session.libsignal.streams.PaddingInputStream
|
||||
import org.session.libsignal.streams.PlaintextOutputStreamFactory
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.session.libsignal.utilities.PushAttachmentData
|
||||
import org.session.libsignal.utilities.Util
|
||||
@@ -45,29 +49,29 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
|
||||
private val MESSAGE_SEND_JOB_ID_KEY = "message_send_job_id"
|
||||
}
|
||||
|
||||
override fun execute() {
|
||||
override suspend fun execute(dispatcherName: String) {
|
||||
try {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
|
||||
val attachment = messageDataProvider.getScaledSignalAttachmentStream(attachmentID)
|
||||
?: return handleFailure(Error.NoAttachment)
|
||||
?: return handleFailure(dispatcherName, Error.NoAttachment)
|
||||
val openGroup = storage.getOpenGroup(threadID.toLong())
|
||||
if (openGroup != null) {
|
||||
val keyAndResult = upload(attachment, openGroup.server, false) {
|
||||
OpenGroupApi.upload(it, openGroup.room, openGroup.server)
|
||||
}
|
||||
handleSuccess(attachment, keyAndResult.first, keyAndResult.second)
|
||||
handleSuccess(dispatcherName, attachment, keyAndResult.first, keyAndResult.second)
|
||||
} else {
|
||||
val keyAndResult = upload(attachment, FileServerApi.server, true) {
|
||||
FileServerApi.upload(it)
|
||||
}
|
||||
handleSuccess(attachment, keyAndResult.first, keyAndResult.second)
|
||||
handleSuccess(dispatcherName, attachment, keyAndResult.first, keyAndResult.second)
|
||||
}
|
||||
} catch (e: java.lang.Exception) {
|
||||
if (e == Error.NoAttachment) {
|
||||
this.handlePermanentFailure(e)
|
||||
this.handlePermanentFailure(dispatcherName, e)
|
||||
} else {
|
||||
this.handleFailure(e)
|
||||
this.handleFailure(dispatcherName, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -104,9 +108,9 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
|
||||
return Pair(key, UploadResult(id, "${server}/file/$id", digest))
|
||||
}
|
||||
|
||||
private fun handleSuccess(attachment: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: UploadResult) {
|
||||
private fun handleSuccess(dispatcherName: String, attachment: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: UploadResult) {
|
||||
Log.d(TAG, "Attachment uploaded successfully.")
|
||||
delegate?.handleJobSucceeded(this)
|
||||
delegate?.handleJobSucceeded(this, dispatcherName)
|
||||
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
|
||||
messageDataProvider.handleSuccessfulAttachmentUpload(attachmentID, attachment, attachmentKey, uploadResult)
|
||||
if (attachment.contentType.startsWith("audio/")) {
|
||||
@@ -144,16 +148,16 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
|
||||
storage.resumeMessageSendJobIfNeeded(messageSendJobID)
|
||||
}
|
||||
|
||||
private fun handlePermanentFailure(e: Exception) {
|
||||
private fun handlePermanentFailure(dispatcherName: String, e: Exception) {
|
||||
Log.w(TAG, "Attachment upload failed permanently due to error: $this.")
|
||||
delegate?.handleJobFailedPermanently(this, e)
|
||||
delegate?.handleJobFailedPermanently(this, dispatcherName, e)
|
||||
MessagingModuleConfiguration.shared.messageDataProvider.handleFailedAttachmentUpload(attachmentID)
|
||||
failAssociatedMessageSendJob(e)
|
||||
}
|
||||
|
||||
private fun handleFailure(e: Exception) {
|
||||
private fun handleFailure(dispatcherName: String, e: Exception) {
|
||||
Log.w(TAG, "Attachment upload failed due to error: $this.")
|
||||
delegate?.handleJobFailed(this, e)
|
||||
delegate?.handleJobFailed(this, dispatcherName, e)
|
||||
if (failureCount + 1 >= maxFailureCount) {
|
||||
failAssociatedMessageSendJob(e)
|
||||
}
|
||||
|
@@ -3,9 +3,7 @@ package org.session.libsession.messaging.jobs
|
||||
import okhttp3.HttpUrl
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.open_groups.OpenGroup
|
||||
import org.session.libsession.messaging.open_groups.OpenGroupApi
|
||||
import org.session.libsession.messaging.utilities.Data
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsession.utilities.OpenGroupUrlParser
|
||||
import org.session.libsignal.utilities.Log
|
||||
|
||||
@@ -29,37 +27,26 @@ class BackgroundGroupAddJob(val joinUrl: String): Job {
|
||||
return "$server.$room"
|
||||
}
|
||||
|
||||
override fun execute() {
|
||||
override suspend fun execute(dispatcherName: String) {
|
||||
try {
|
||||
val openGroup = OpenGroupUrlParser.parseUrl(joinUrl)
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val allOpenGroups = storage.getAllOpenGroups().map { it.value.joinURL }
|
||||
if (allOpenGroups.contains(openGroup.joinUrl())) {
|
||||
Log.e("OpenGroupDispatcher", "Failed to add group because", DuplicateGroupException())
|
||||
delegate?.handleJobFailed(this, DuplicateGroupException())
|
||||
delegate?.handleJobFailed(this, dispatcherName, DuplicateGroupException())
|
||||
return
|
||||
}
|
||||
// get image
|
||||
storage.setOpenGroupPublicKey(openGroup.server, openGroup.serverPublicKey)
|
||||
val (capabilities, info) = OpenGroupApi.getCapabilitiesAndRoomInfo(openGroup.room, openGroup.server).get()
|
||||
storage.setServerCapabilities(openGroup.server, capabilities.capabilities)
|
||||
val imageId = info.imageId
|
||||
|
||||
storage.addOpenGroup(openGroup.joinUrl())
|
||||
if (imageId != null) {
|
||||
val bytes = OpenGroupApi.downloadOpenGroupProfilePicture(openGroup.server, openGroup.room, imageId).get()
|
||||
val groupId = GroupUtil.getEncodedOpenGroupID("${openGroup.server}.${openGroup.room}".toByteArray())
|
||||
storage.updateProfilePicture(groupId, bytes)
|
||||
storage.updateTimestampUpdated(groupId, System.currentTimeMillis())
|
||||
}
|
||||
Log.d(KEY, "onOpenGroupAdded(${openGroup.server})")
|
||||
storage.onOpenGroupAdded(openGroup.server)
|
||||
storage.onOpenGroupAdded(openGroup.server, openGroup.room)
|
||||
} catch (e: Exception) {
|
||||
Log.e("OpenGroupDispatcher", "Failed to add group because",e)
|
||||
delegate?.handleJobFailed(this, e)
|
||||
delegate?.handleJobFailed(this, dispatcherName, e)
|
||||
return
|
||||
}
|
||||
Log.d("Loki", "Group added successfully")
|
||||
delegate?.handleJobSucceeded(this)
|
||||
delegate?.handleJobSucceeded(this, dispatcherName)
|
||||
}
|
||||
|
||||
override fun serialize(): Data = Data.Builder()
|
||||
|
@@ -7,16 +7,25 @@ import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.task
|
||||
import org.session.libsession.database.StorageProtocol
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.messages.Message
|
||||
import org.session.libsession.messaging.messages.control.CallMessage
|
||||
import org.session.libsession.messaging.messages.control.ClosedGroupControlMessage
|
||||
import org.session.libsession.messaging.messages.control.ConfigurationMessage
|
||||
import org.session.libsession.messaging.messages.control.DataExtractionNotification
|
||||
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate
|
||||
import org.session.libsession.messaging.messages.control.MessageRequestResponse
|
||||
import org.session.libsession.messaging.messages.control.ReadReceipt
|
||||
import org.session.libsession.messaging.messages.control.SharedConfigurationMessage
|
||||
import org.session.libsession.messaging.messages.control.TypingIndicator
|
||||
import org.session.libsession.messaging.messages.control.UnsendRequest
|
||||
import org.session.libsession.messaging.messages.visible.ParsedMessage
|
||||
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
||||
import org.session.libsession.messaging.open_groups.OpenGroupApi
|
||||
import org.session.libsession.messaging.sending_receiving.MessageReceiver
|
||||
import org.session.libsession.messaging.sending_receiving.handle
|
||||
import org.session.libsession.messaging.sending_receiving.handleOpenGroupReactions
|
||||
import org.session.libsession.messaging.sending_receiving.handleUnsendRequest
|
||||
import org.session.libsession.messaging.sending_receiving.handleVisibleMessage
|
||||
import org.session.libsession.messaging.utilities.Data
|
||||
import org.session.libsession.messaging.utilities.SessionId
|
||||
@@ -25,6 +34,7 @@ import org.session.libsession.utilities.SSKEnvironment
|
||||
import org.session.libsignal.protos.UtilProtos
|
||||
import org.session.libsignal.utilities.IdPrefix
|
||||
import org.session.libsignal.utilities.Log
|
||||
import kotlin.math.max
|
||||
|
||||
data class MessageReceiveParameters(
|
||||
val data: ByteArray,
|
||||
@@ -51,6 +61,9 @@ class BatchMessageReceiveJob(
|
||||
|
||||
const val BATCH_DEFAULT_NUMBER = 512
|
||||
|
||||
// used for processing messages that don't have a thread and shouldn't create one
|
||||
const val NO_THREAD_MAPPING = -1L
|
||||
|
||||
// Keys used for database storage
|
||||
private val NUM_MESSAGES_KEY = "numMessages"
|
||||
private val DATA_KEY = "data"
|
||||
@@ -59,116 +72,182 @@ class BatchMessageReceiveJob(
|
||||
private val OPEN_GROUP_ID_KEY = "open_group_id"
|
||||
}
|
||||
|
||||
private fun getThreadId(message: Message, storage: StorageProtocol): Long {
|
||||
val senderOrSync = when (message) {
|
||||
is VisibleMessage -> message.syncTarget ?: message.sender!!
|
||||
is ExpirationTimerUpdate -> message.syncTarget ?: message.sender!!
|
||||
else -> message.sender!!
|
||||
private fun shouldCreateThread(parsedMessage: ParsedMessage): Boolean {
|
||||
val message = parsedMessage.message
|
||||
if (message is VisibleMessage) return true
|
||||
else { // message is control message otherwise
|
||||
return when(message) {
|
||||
is SharedConfigurationMessage -> false
|
||||
is ClosedGroupControlMessage -> false // message.kind is ClosedGroupControlMessage.Kind.New && !message.isSenderSelf
|
||||
is DataExtractionNotification -> false
|
||||
is MessageRequestResponse -> false
|
||||
is ExpirationTimerUpdate -> false
|
||||
is ConfigurationMessage -> false
|
||||
is TypingIndicator -> false
|
||||
is UnsendRequest -> false
|
||||
is ReadReceipt -> false
|
||||
is CallMessage -> false // TODO: maybe
|
||||
else -> false // shouldn't happen, or I guess would be Visible
|
||||
}
|
||||
}
|
||||
return storage.getOrCreateThreadIdFor(senderOrSync, message.groupPublicKey, openGroupID)
|
||||
}
|
||||
|
||||
override fun execute() {
|
||||
executeAsync().get()
|
||||
override suspend fun execute(dispatcherName: String) {
|
||||
executeAsync(dispatcherName).get()
|
||||
}
|
||||
|
||||
fun executeAsync(): Promise<Unit, Exception> {
|
||||
fun executeAsync(dispatcherName: String): Promise<Unit, Exception> {
|
||||
return task {
|
||||
val threadMap = mutableMapOf<Long, MutableList<ParsedMessage>>()
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val context = MessagingModuleConfiguration.shared.context
|
||||
val localUserPublicKey = storage.getUserPublicKey()
|
||||
val serverPublicKey = openGroupID?.let { storage.getOpenGroupPublicKey(it.split(".").dropLast(1).joinToString(".")) }
|
||||
val currentClosedGroups = storage.getAllActiveClosedGroupPublicKeys()
|
||||
|
||||
// parse and collect IDs
|
||||
messages.forEach { messageParameters ->
|
||||
val (data, serverHash, openGroupMessageServerID) = messageParameters
|
||||
try {
|
||||
val (message, proto) = MessageReceiver.parse(data, openGroupMessageServerID, openGroupPublicKey = serverPublicKey)
|
||||
val (message, proto) = MessageReceiver.parse(data, openGroupMessageServerID, openGroupPublicKey = serverPublicKey, currentClosedGroups = currentClosedGroups)
|
||||
message.serverHash = serverHash
|
||||
val threadID = getThreadId(message, storage)
|
||||
val parsedParams = ParsedMessage(messageParameters, message, proto)
|
||||
val threadID = Message.getThreadId(message, openGroupID, storage, shouldCreateThread(parsedParams)) ?: NO_THREAD_MAPPING
|
||||
if (!threadMap.containsKey(threadID)) {
|
||||
threadMap[threadID] = mutableListOf(parsedParams)
|
||||
} else {
|
||||
threadMap[threadID]!! += parsedParams
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Couldn't receive message.", e)
|
||||
if (e is MessageReceiver.Error && !e.isRetryable) {
|
||||
Log.e(TAG, "Message failed permanently",e)
|
||||
} else {
|
||||
Log.e(TAG, "Message failed",e)
|
||||
failures += messageParameters
|
||||
when (e) {
|
||||
is MessageReceiver.Error.DuplicateMessage, MessageReceiver.Error.SelfSend -> {
|
||||
Log.i(TAG, "Couldn't receive message, failed with error: ${e.message} (id: $id)")
|
||||
}
|
||||
is MessageReceiver.Error -> {
|
||||
if (!e.isRetryable) {
|
||||
Log.e(TAG, "Couldn't receive message, failed permanently (id: $id)", e)
|
||||
}
|
||||
else {
|
||||
Log.e(TAG, "Couldn't receive message, failed (id: $id)", e)
|
||||
failures += messageParameters
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
Log.e(TAG, "Couldn't receive message, failed (id: $id)", e)
|
||||
failures += messageParameters
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// iterate over threads and persist them (persistence is the longest constant in the batch process operation)
|
||||
runBlocking(Dispatchers.IO) {
|
||||
val deferredThreadMap = threadMap.entries.map { (threadId, messages) ->
|
||||
async {
|
||||
val messageIds = mutableListOf<Pair<Long, Boolean>>()
|
||||
messages.forEach { (parameters, message, proto) ->
|
||||
try {
|
||||
if (message is VisibleMessage) {
|
||||
|
||||
fun processMessages(threadId: Long, messages: List<ParsedMessage>) = async {
|
||||
// The LinkedHashMap should preserve insertion order
|
||||
val messageIds = linkedMapOf<Long, Pair<Boolean, Boolean>>()
|
||||
val myLastSeen = storage.getLastSeen(threadId)
|
||||
var newLastSeen = myLastSeen.takeUnless { it == -1L } ?: 0
|
||||
messages.forEach { (parameters, message, proto) ->
|
||||
try {
|
||||
when (message) {
|
||||
is VisibleMessage -> {
|
||||
val isUserBlindedSender =
|
||||
message.sender == serverPublicKey?.let {
|
||||
SodiumUtilities.blindedKeyPair(
|
||||
it,
|
||||
MessagingModuleConfiguration.shared.getUserED25519KeyPair()!!
|
||||
)
|
||||
}?.let {
|
||||
SessionId(
|
||||
IdPrefix.BLINDED, it.publicKey.asBytes
|
||||
).hexString
|
||||
}
|
||||
if (message.sender == localUserPublicKey || isUserBlindedSender) {
|
||||
// use sent timestamp here since that is technically the last one we have
|
||||
newLastSeen = max(newLastSeen, message.sentTimestamp!!)
|
||||
}
|
||||
val messageId = MessageReceiver.handleVisibleMessage(message, proto, openGroupID,
|
||||
runIncrement = false,
|
||||
threadId,
|
||||
runThreadUpdate = false,
|
||||
runProfileUpdate = true
|
||||
)
|
||||
runProfileUpdate = true)
|
||||
|
||||
if (messageId != null && message.reaction == null) {
|
||||
val isUserBlindedSender = message.sender == serverPublicKey?.let { SodiumUtilities.blindedKeyPair(it, MessagingModuleConfiguration.shared.getUserED25519KeyPair()!!) }?.let { SessionId(
|
||||
IdPrefix.BLINDED, it.publicKey.asBytes).hexString }
|
||||
messageIds += messageId to (message.sender == localUserPublicKey || isUserBlindedSender)
|
||||
messageIds[messageId] = Pair(
|
||||
(message.sender == localUserPublicKey || isUserBlindedSender),
|
||||
message.hasMention
|
||||
)
|
||||
}
|
||||
parameters.openGroupMessageServerID?.let {
|
||||
MessageReceiver.handleOpenGroupReactions(threadId, it, parameters.reactions)
|
||||
MessageReceiver.handleOpenGroupReactions(
|
||||
threadId,
|
||||
it,
|
||||
parameters.reactions
|
||||
)
|
||||
}
|
||||
} else {
|
||||
MessageReceiver.handle(message, proto, openGroupID)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Couldn't process message.", e)
|
||||
if (e is MessageReceiver.Error && !e.isRetryable) {
|
||||
Log.e(TAG, "Message failed permanently",e)
|
||||
} else {
|
||||
Log.e(TAG, "Message failed",e)
|
||||
failures += parameters
|
||||
|
||||
is UnsendRequest -> {
|
||||
val deletedMessageId =
|
||||
MessageReceiver.handleUnsendRequest(message)
|
||||
|
||||
// If we removed a message then ensure it isn't in the 'messageIds'
|
||||
if (deletedMessageId != null) {
|
||||
messageIds.remove(deletedMessageId)
|
||||
}
|
||||
}
|
||||
|
||||
else -> MessageReceiver.handle(message, proto, threadId, openGroupID)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Couldn't process message (id: $id)", e)
|
||||
if (e is MessageReceiver.Error && !e.isRetryable) {
|
||||
Log.e(TAG, "Message failed permanently (id: $id)", e)
|
||||
} else {
|
||||
Log.e(TAG, "Message failed (id: $id)", e)
|
||||
failures += parameters
|
||||
}
|
||||
}
|
||||
// increment unreads, notify, and update thread
|
||||
val unreadFromMine = messageIds.indexOfLast { (_,fromMe) -> fromMe }
|
||||
var trueUnreadCount = messageIds.filter { (_,fromMe) -> !fromMe }.size
|
||||
if (unreadFromMine >= 0) {
|
||||
trueUnreadCount -= (unreadFromMine + 1)
|
||||
storage.markConversationAsRead(threadId, false)
|
||||
}
|
||||
if (trueUnreadCount > 0) {
|
||||
storage.incrementUnread(threadId, trueUnreadCount)
|
||||
}
|
||||
storage.updateThread(threadId, true)
|
||||
SSKEnvironment.shared.notificationManager.updateNotification(context, threadId)
|
||||
}
|
||||
// increment unreads, notify, and update thread
|
||||
// last seen will be the current last seen if not changed (re-computes the read counts for thread record)
|
||||
// might have been updated from a different thread at this point
|
||||
val currentLastSeen = storage.getLastSeen(threadId).let { if (it == -1L) 0 else it }
|
||||
newLastSeen = max(newLastSeen, currentLastSeen)
|
||||
if (newLastSeen > 0 || currentLastSeen == 0L) {
|
||||
storage.markConversationAsRead(threadId, newLastSeen, force = true)
|
||||
}
|
||||
storage.updateThread(threadId, true)
|
||||
SSKEnvironment.shared.notificationManager.updateNotification(context, threadId)
|
||||
}
|
||||
|
||||
val withoutDefault = threadMap.entries.filter { it.key != NO_THREAD_MAPPING }
|
||||
val noThreadMessages = threadMap[NO_THREAD_MAPPING] ?: listOf()
|
||||
val deferredThreadMap = withoutDefault.map { (threadId, messages) ->
|
||||
processMessages(threadId, messages)
|
||||
}
|
||||
// await all thread processing
|
||||
deferredThreadMap.awaitAll()
|
||||
if (noThreadMessages.isNotEmpty()) {
|
||||
processMessages(NO_THREAD_MAPPING, noThreadMessages).await()
|
||||
}
|
||||
}
|
||||
if (failures.isEmpty()) {
|
||||
handleSuccess()
|
||||
handleSuccess(dispatcherName)
|
||||
} else {
|
||||
handleFailure()
|
||||
handleFailure(dispatcherName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSuccess() {
|
||||
this.delegate?.handleJobSucceeded(this)
|
||||
private fun handleSuccess(dispatcherName: String) {
|
||||
Log.i(TAG, "Completed processing of ${messages.size} messages (id: $id)")
|
||||
delegate?.handleJobSucceeded(this, dispatcherName)
|
||||
}
|
||||
|
||||
private fun handleFailure() {
|
||||
this.delegate?.handleJobFailed(this, Exception("One or more jobs resulted in failure"))
|
||||
private fun handleFailure(dispatcherName: String) {
|
||||
Log.i(TAG, "Handling failure of ${failures.size} messages (${messages.size - failures.size} processed successfully) (id: $id)")
|
||||
delegate?.handleJobFailed(this, dispatcherName, Exception("One or more jobs resulted in failure"))
|
||||
}
|
||||
|
||||
override fun serialize(): Data {
|
||||
@@ -201,10 +280,9 @@ class BatchMessageReceiveJob(
|
||||
val openGroupID = data.getStringOrDefault(OPEN_GROUP_ID_KEY, null)
|
||||
|
||||
val parameters = (0 until numMessages).map { index ->
|
||||
val data = contents[index]
|
||||
val serverHash = serverHashes[index].let { if (it.isEmpty()) null else it }
|
||||
val serverId = openGroupMessageServerIDs[index].let { if (it == -1L) null else it }
|
||||
MessageReceiveParameters(data, serverHash, serverId)
|
||||
MessageReceiveParameters(contents[index], serverHash, serverId)
|
||||
}
|
||||
|
||||
return BatchMessageReceiveJob(parameters, openGroupID)
|
||||
|
@@ -0,0 +1,206 @@
|
||||
package org.session.libsession.messaging.jobs
|
||||
|
||||
import network.loki.messenger.libsession_util.ConfigBase
|
||||
import network.loki.messenger.libsession_util.ConfigBase.Companion.protoKindFor
|
||||
import nl.komponents.kovenant.functional.bind
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.messages.Destination
|
||||
import org.session.libsession.messaging.messages.control.SharedConfigurationMessage
|
||||
import org.session.libsession.messaging.sending_receiving.MessageSender
|
||||
import org.session.libsession.messaging.utilities.Data
|
||||
import org.session.libsession.snode.RawResponse
|
||||
import org.session.libsession.snode.SnodeAPI
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.utilities.Log
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
// only contact (self) and closed group destinations will be supported
|
||||
data class ConfigurationSyncJob(val destination: Destination): Job {
|
||||
|
||||
override var delegate: JobDelegate? = null
|
||||
override var id: String? = null
|
||||
override var failureCount: Int = 0
|
||||
override val maxFailureCount: Int = 10
|
||||
|
||||
val shouldRunAgain = AtomicBoolean(false)
|
||||
|
||||
override suspend fun execute(dispatcherName: String) {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val forcedConfig = TextSecurePreferences.hasForcedNewConfig(MessagingModuleConfiguration.shared.context)
|
||||
val currentTime = SnodeAPI.nowWithOffset
|
||||
val userEdKeyPair = MessagingModuleConfiguration.shared.getUserED25519KeyPair()
|
||||
val userPublicKey = storage.getUserPublicKey()
|
||||
val delegate = delegate
|
||||
if (destination is Destination.ClosedGroup // TODO: closed group configs will be handled in closed group feature
|
||||
// if we haven't enabled the new configs don't run
|
||||
|| !ConfigBase.isNewConfigEnabled(forcedConfig, currentTime)
|
||||
// if we don't have a user ed key pair for signing updates
|
||||
|| userEdKeyPair == null
|
||||
// this will be useful to not handle null delegate cases
|
||||
|| delegate == null
|
||||
// check our local identity key exists
|
||||
|| userPublicKey.isNullOrEmpty()
|
||||
// don't allow pushing configs for non-local user
|
||||
|| (destination is Destination.Contact && destination.publicKey != userPublicKey)
|
||||
) {
|
||||
Log.w(TAG, "No need to run config sync job, TODO")
|
||||
return delegate?.handleJobSucceeded(this, dispatcherName) ?: Unit
|
||||
}
|
||||
|
||||
// configFactory singleton instance will come in handy for modifying hashes and fetching configs for namespace etc
|
||||
val configFactory = MessagingModuleConfiguration.shared.configFactory
|
||||
|
||||
// get latest states, filter out configs that don't need push
|
||||
val configsRequiringPush = configFactory.getUserConfigs().filter { config -> config.needsPush() }
|
||||
|
||||
// don't run anything if we don't need to push anything
|
||||
if (configsRequiringPush.isEmpty()) return delegate.handleJobSucceeded(this, dispatcherName)
|
||||
|
||||
// need to get the current hashes before we call `push()`
|
||||
val toDeleteHashes = mutableListOf<String>()
|
||||
|
||||
// allow null results here so the list index matches configsRequiringPush
|
||||
val sentTimestamp: Long = SnodeAPI.nowWithOffset
|
||||
val batchObjects: List<Pair<SharedConfigurationMessage, SnodeAPI.SnodeBatchRequestInfo>?> = configsRequiringPush.map { config ->
|
||||
val (data, seqNo, obsoleteHashes) = config.push()
|
||||
toDeleteHashes += obsoleteHashes
|
||||
SharedConfigurationMessage(config.protoKindFor(), data, seqNo) to config
|
||||
}.map { (message, config) ->
|
||||
// return a list of batch request objects
|
||||
val snodeMessage = MessageSender.buildWrappedMessageToSnode(destination, message, true)
|
||||
val authenticated = SnodeAPI.buildAuthenticatedStoreBatchInfo(
|
||||
destination.destinationPublicKey(),
|
||||
config.configNamespace(),
|
||||
snodeMessage
|
||||
) ?: return@map null // this entry will be null otherwise
|
||||
message to authenticated // to keep track of seqNo for calling confirmPushed later
|
||||
}
|
||||
|
||||
val toDeleteRequest = toDeleteHashes.let { toDeleteFromAllNamespaces ->
|
||||
if (toDeleteFromAllNamespaces.isEmpty()) null
|
||||
else SnodeAPI.buildAuthenticatedDeleteBatchInfo(destination.destinationPublicKey(), toDeleteFromAllNamespaces)
|
||||
}
|
||||
|
||||
if (batchObjects.any { it == null }) {
|
||||
// stop running here, something like a signing error occurred
|
||||
return delegate.handleJobFailedPermanently(this, dispatcherName, NullPointerException("One or more requests had a null batch request info"))
|
||||
}
|
||||
|
||||
val allRequests = mutableListOf<SnodeAPI.SnodeBatchRequestInfo>()
|
||||
allRequests += batchObjects.requireNoNulls().map { (_, request) -> request }
|
||||
// add in the deletion if we have any hashes
|
||||
if (toDeleteRequest != null) {
|
||||
allRequests += toDeleteRequest
|
||||
Log.d(TAG, "Including delete request for current hashes")
|
||||
}
|
||||
|
||||
val batchResponse = SnodeAPI.getSingleTargetSnode(destination.destinationPublicKey()).bind { snode ->
|
||||
SnodeAPI.getRawBatchResponse(
|
||||
snode,
|
||||
destination.destinationPublicKey(),
|
||||
allRequests,
|
||||
sequence = true
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
val rawResponses = batchResponse.get()
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val responseList = (rawResponses["results"] as List<RawResponse>)
|
||||
// we are always adding in deletions at the end
|
||||
val deletionResponse = if (toDeleteRequest != null && responseList.isNotEmpty()) responseList.last() else null
|
||||
val deletedHashes = deletionResponse?.let {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
// get the sub-request body
|
||||
(deletionResponse["body"] as? RawResponse)?.let { body ->
|
||||
// get the swarm dict
|
||||
body["swarm"] as? RawResponse
|
||||
}?.mapValues { (_, swarmDict) ->
|
||||
// get the deleted values from dict
|
||||
((swarmDict as? RawResponse)?.get("deleted") as? List<String>)?.toSet() ?: emptySet()
|
||||
}?.values?.reduce { acc, strings ->
|
||||
// create an intersection of all deleted hashes (common between all swarm nodes)
|
||||
acc intersect strings
|
||||
}
|
||||
} ?: emptySet()
|
||||
|
||||
// at this point responseList index should line up with configsRequiringPush index
|
||||
configsRequiringPush.forEachIndexed { index, config ->
|
||||
val (toPushMessage, _) = batchObjects[index]!!
|
||||
val response = responseList[index]
|
||||
val responseBody = response["body"] as? RawResponse
|
||||
val insertHash = responseBody?.get("hash") as? String ?: run {
|
||||
Log.w(TAG, "No hash returned for the configuration in namespace ${config.configNamespace()}")
|
||||
return@forEachIndexed
|
||||
}
|
||||
Log.d(TAG, "Hash ${insertHash.take(4)} returned from store request for new config")
|
||||
|
||||
// confirm pushed seqno
|
||||
val thisSeqNo = toPushMessage.seqNo
|
||||
config.confirmPushed(thisSeqNo, insertHash)
|
||||
Log.d(TAG, "Successfully removed the deleted hashes from ${config.javaClass.simpleName}")
|
||||
// dump and write config after successful
|
||||
if (config.needsDump()) { // usually this will be true?
|
||||
configFactory.persist(config, toPushMessage.sentTimestamp ?: sentTimestamp)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error performing batch request", e)
|
||||
return delegate.handleJobFailed(this, dispatcherName, e)
|
||||
}
|
||||
delegate.handleJobSucceeded(this, dispatcherName)
|
||||
if (shouldRunAgain.get() && storage.getConfigSyncJob(destination) == null) {
|
||||
// reschedule if something has updated since we started this job
|
||||
JobQueue.shared.add(ConfigurationSyncJob(destination))
|
||||
}
|
||||
}
|
||||
|
||||
fun Destination.destinationPublicKey(): String = when (this) {
|
||||
is Destination.Contact -> publicKey
|
||||
is Destination.ClosedGroup -> groupPublicKey
|
||||
else -> throw NullPointerException("Not public key for this destination")
|
||||
}
|
||||
|
||||
override fun serialize(): Data {
|
||||
val (type, address) = when (destination) {
|
||||
is Destination.Contact -> CONTACT_TYPE to destination.publicKey
|
||||
is Destination.ClosedGroup -> GROUP_TYPE to destination.groupPublicKey
|
||||
else -> return Data.EMPTY
|
||||
}
|
||||
return Data.Builder()
|
||||
.putInt(DESTINATION_TYPE_KEY, type)
|
||||
.putString(DESTINATION_ADDRESS_KEY, address)
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun getFactoryKey(): String = KEY
|
||||
|
||||
companion object {
|
||||
const val TAG = "ConfigSyncJob"
|
||||
const val KEY = "ConfigSyncJob"
|
||||
|
||||
// Keys used for DB storage
|
||||
const val DESTINATION_ADDRESS_KEY = "destinationAddress"
|
||||
const val DESTINATION_TYPE_KEY = "destinationType"
|
||||
|
||||
// type mappings
|
||||
const val CONTACT_TYPE = 1
|
||||
const val GROUP_TYPE = 2
|
||||
|
||||
}
|
||||
|
||||
class Factory: Job.Factory<ConfigurationSyncJob> {
|
||||
override fun create(data: Data): ConfigurationSyncJob? {
|
||||
if (!data.hasInt(DESTINATION_TYPE_KEY) || !data.hasString(DESTINATION_ADDRESS_KEY)) return null
|
||||
|
||||
val address = data.getString(DESTINATION_ADDRESS_KEY)
|
||||
val destination = when (data.getInt(DESTINATION_TYPE_KEY)) {
|
||||
CONTACT_TYPE -> Destination.Contact(address)
|
||||
GROUP_TYPE -> Destination.ClosedGroup(address)
|
||||
else -> return null
|
||||
}
|
||||
|
||||
return ConfigurationSyncJob(destination)
|
||||
}
|
||||
}
|
||||
}
|
@@ -3,27 +3,51 @@ package org.session.libsession.messaging.jobs
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.open_groups.OpenGroupApi
|
||||
import org.session.libsession.messaging.utilities.Data
|
||||
import org.session.libsession.snode.SnodeAPI
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
|
||||
class GroupAvatarDownloadJob(val room: String, val server: String) : Job {
|
||||
class GroupAvatarDownloadJob(val server: String, val room: String, val imageId: String?) : Job {
|
||||
|
||||
override var delegate: JobDelegate? = null
|
||||
override var id: String? = null
|
||||
override var failureCount: Int = 0
|
||||
override val maxFailureCount: Int = 10
|
||||
|
||||
override fun execute() {
|
||||
override suspend fun execute(dispatcherName: String) {
|
||||
if (imageId == null) {
|
||||
delegate?.handleJobFailedPermanently(this, dispatcherName, Exception("GroupAvatarDownloadJob now requires imageId"))
|
||||
return
|
||||
}
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val openGroup = storage.getOpenGroup(room, server)
|
||||
if (openGroup == null || storage.getThreadId(openGroup) == null) {
|
||||
delegate?.handleJobFailedPermanently(this, dispatcherName, Exception("GroupAvatarDownloadJob openGroup is null"))
|
||||
return
|
||||
}
|
||||
val storedImageId = openGroup.imageId
|
||||
|
||||
if (storedImageId == null || storedImageId != imageId) {
|
||||
delegate?.handleJobFailedPermanently(this, dispatcherName, Exception("GroupAvatarDownloadJob imageId does not match the OpenGroup"))
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
val info = OpenGroupApi.getRoomInfo(room, server).get()
|
||||
val imageId = info.imageId ?: return
|
||||
val bytes = OpenGroupApi.downloadOpenGroupProfilePicture(server, info.token, imageId).get()
|
||||
val bytes = OpenGroupApi.downloadOpenGroupProfilePicture(server, room, imageId).get()
|
||||
|
||||
// Once the download is complete the imageId might no longer match, so we need to fetch it again just in case
|
||||
val postDownloadStoredImageId = storage.getOpenGroup(room, server)?.imageId
|
||||
|
||||
if (postDownloadStoredImageId == null || postDownloadStoredImageId != imageId) {
|
||||
delegate?.handleJobFailedPermanently(this, dispatcherName, Exception("GroupAvatarDownloadJob imageId no longer matches the OpenGroup"))
|
||||
return
|
||||
}
|
||||
|
||||
val groupId = GroupUtil.getEncodedOpenGroupID("$server.$room".toByteArray())
|
||||
storage.updateProfilePicture(groupId, bytes)
|
||||
storage.updateTimestampUpdated(groupId, System.currentTimeMillis())
|
||||
delegate?.handleJobSucceeded(this)
|
||||
storage.updateTimestampUpdated(groupId, SnodeAPI.nowWithOffset)
|
||||
delegate?.handleJobSucceeded(this, dispatcherName)
|
||||
} catch (e: Exception) {
|
||||
delegate?.handleJobFailed(this, e)
|
||||
delegate?.handleJobFailed(this, dispatcherName, e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +55,7 @@ class GroupAvatarDownloadJob(val room: String, val server: String) : Job {
|
||||
return Data.Builder()
|
||||
.putString(ROOM, room)
|
||||
.putString(SERVER, server)
|
||||
.putString(IMAGE_ID, imageId)
|
||||
.build()
|
||||
}
|
||||
|
||||
@@ -41,14 +66,16 @@ class GroupAvatarDownloadJob(val room: String, val server: String) : Job {
|
||||
|
||||
private const val ROOM = "room"
|
||||
private const val SERVER = "server"
|
||||
private const val IMAGE_ID = "imageId"
|
||||
}
|
||||
|
||||
class Factory : Job.Factory<GroupAvatarDownloadJob> {
|
||||
|
||||
override fun create(data: Data): GroupAvatarDownloadJob {
|
||||
return GroupAvatarDownloadJob(
|
||||
data.getString(SERVER),
|
||||
data.getString(ROOM),
|
||||
data.getString(SERVER)
|
||||
if (data.hasString(IMAGE_ID)) { data.getString(IMAGE_ID) } else { null }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@@ -17,7 +17,7 @@ interface Job {
|
||||
internal const val MAX_BUFFER_SIZE = 1_000_000 // bytes
|
||||
}
|
||||
|
||||
fun execute()
|
||||
suspend fun execute(dispatcherName: String)
|
||||
|
||||
fun serialize(): Data
|
||||
|
||||
|
@@ -2,7 +2,7 @@ package org.session.libsession.messaging.jobs
|
||||
|
||||
interface JobDelegate {
|
||||
|
||||
fun handleJobSucceeded(job: Job)
|
||||
fun handleJobFailed(job: Job, error: Exception)
|
||||
fun handleJobFailedPermanently(job: Job, error: Exception)
|
||||
fun handleJobSucceeded(job: Job, dispatcherName: String)
|
||||
fun handleJobFailed(job: Job, dispatcherName: String, error: Exception)
|
||||
fun handleJobFailedPermanently(job: Job, dispatcherName: String, error: Exception)
|
||||
}
|
@@ -26,7 +26,7 @@ class JobQueue : JobDelegate {
|
||||
private val jobTimestampMap = ConcurrentHashMap<Long, AtomicInteger>()
|
||||
private val rxDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
|
||||
private val rxMediaDispatcher = Executors.newFixedThreadPool(4).asCoroutineDispatcher()
|
||||
private val openGroupDispatcher = Executors.newCachedThreadPool().asCoroutineDispatcher()
|
||||
private val openGroupDispatcher = Executors.newFixedThreadPool(8).asCoroutineDispatcher()
|
||||
private val txDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
|
||||
private val scope = CoroutineScope(Dispatchers.Default) + SupervisorJob()
|
||||
private val queue = Channel<Job>(UNLIMITED)
|
||||
@@ -53,7 +53,7 @@ class JobQueue : JobDelegate {
|
||||
}
|
||||
if (openGroupId.isNullOrEmpty()) {
|
||||
Log.e("OpenGroupDispatcher", "Open Group ID was null on ${job.javaClass.simpleName}")
|
||||
handleJobFailedPermanently(job, NullPointerException("Open Group ID was null"))
|
||||
handleJobFailedPermanently(job, name, NullPointerException("Open Group ID was null"))
|
||||
} else {
|
||||
val groupChannel = if (!openGroupChannels.containsKey(openGroupId)) {
|
||||
Log.d("OpenGroupDispatcher", "Creating ${openGroupId.hashCode()} channel")
|
||||
@@ -94,10 +94,17 @@ class JobQueue : JobDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
private fun Job.process(dispatcherName: String) {
|
||||
Log.d(dispatcherName,"processJob: ${javaClass.simpleName}")
|
||||
private suspend fun Job.process(dispatcherName: String) {
|
||||
Log.d(dispatcherName,"processJob: ${javaClass.simpleName} (id: $id)")
|
||||
delegate = this@JobQueue
|
||||
execute()
|
||||
|
||||
try {
|
||||
execute(dispatcherName)
|
||||
}
|
||||
catch (e: Exception) {
|
||||
Log.d(dispatcherName, "unhandledJobException: ${javaClass.simpleName} (id: $id)")
|
||||
this@JobQueue.handleJobFailed(this, dispatcherName, e)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
@@ -115,9 +122,10 @@ class JobQueue : JobDelegate {
|
||||
|
||||
while (isActive) {
|
||||
when (val job = queue.receive()) {
|
||||
is NotifyPNServerJob, is AttachmentUploadJob, is MessageSendJob -> {
|
||||
is NotifyPNServerJob, is AttachmentUploadJob, is MessageSendJob, is ConfigurationSyncJob -> {
|
||||
txQueue.send(job)
|
||||
}
|
||||
is RetrieveProfileAvatarJob,
|
||||
is AttachmentDownloadJob -> {
|
||||
mediaQueue.send(job)
|
||||
}
|
||||
@@ -136,7 +144,7 @@ class JobQueue : JobDelegate {
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
throw IllegalStateException("Unexpected job type.")
|
||||
throw IllegalStateException("Unexpected job type: ${job.getFactoryKey()}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -177,7 +185,7 @@ class JobQueue : JobDelegate {
|
||||
return
|
||||
}
|
||||
if (!pendingJobIds.add(id)) {
|
||||
Log.e("Loki","tried to re-queue pending/in-progress job")
|
||||
Log.e("Loki","tried to re-queue pending/in-progress job (id: $id)")
|
||||
return
|
||||
}
|
||||
queue.trySend(job)
|
||||
@@ -196,7 +204,7 @@ class JobQueue : JobDelegate {
|
||||
}
|
||||
}
|
||||
pendingJobs.sortedBy { it.id }.forEach { job ->
|
||||
Log.i("Loki", "Resuming pending job of type: ${job::class.simpleName}.")
|
||||
Log.i("Loki", "Resuming pending job of type: ${job::class.simpleName} (id: ${job.id}).")
|
||||
queue.trySend(job) // Offer always called on unlimited capacity
|
||||
}
|
||||
}
|
||||
@@ -217,27 +225,29 @@ class JobQueue : JobDelegate {
|
||||
GroupAvatarDownloadJob.KEY,
|
||||
BackgroundGroupAddJob.KEY,
|
||||
OpenGroupDeleteJob.KEY,
|
||||
RetrieveProfileAvatarJob.KEY,
|
||||
ConfigurationSyncJob.KEY,
|
||||
)
|
||||
allJobTypes.forEach { type ->
|
||||
resumePendingJobs(type)
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleJobSucceeded(job: Job) {
|
||||
override fun handleJobSucceeded(job: Job, dispatcherName: String) {
|
||||
val jobId = job.id ?: return
|
||||
MessagingModuleConfiguration.shared.storage.markJobAsSucceeded(jobId)
|
||||
pendingJobIds.remove(jobId)
|
||||
}
|
||||
|
||||
override fun handleJobFailed(job: Job, error: Exception) {
|
||||
override fun handleJobFailed(job: Job, dispatcherName: String, error: Exception) {
|
||||
// Canceled
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
if (storage.isJobCanceled(job)) {
|
||||
return Log.i("Loki", "${job::class.simpleName} canceled.")
|
||||
return Log.i("Loki", "${job::class.simpleName} canceled (id: ${job.id}).")
|
||||
}
|
||||
// Message send jobs waiting for the attachment to upload
|
||||
if (job is MessageSendJob && error is MessageSendJob.AwaitingAttachmentUploadException) {
|
||||
Log.i("Loki", "Message send job waiting for attachment upload to finish.")
|
||||
Log.i("Loki", "Message send job waiting for attachment upload to finish (id: ${job.id}).")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -255,21 +265,22 @@ class JobQueue : JobDelegate {
|
||||
job.failureCount += 1
|
||||
|
||||
if (job.failureCount >= job.maxFailureCount) {
|
||||
handleJobFailedPermanently(job, error)
|
||||
handleJobFailedPermanently(job, dispatcherName, error)
|
||||
} else {
|
||||
storage.persistJob(job)
|
||||
val retryInterval = getRetryInterval(job)
|
||||
Log.i("Loki", "${job::class.simpleName} failed; scheduling retry (failure count is ${job.failureCount}).")
|
||||
Log.i("Loki", "${job::class.simpleName} failed (id: ${job.id}); scheduling retry (failure count is ${job.failureCount}).")
|
||||
timer.schedule(delay = retryInterval) {
|
||||
Log.i("Loki", "Retrying ${job::class.simpleName}.")
|
||||
Log.i("Loki", "Retrying ${job::class.simpleName} (id: ${job.id}).")
|
||||
queue.trySend(job)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleJobFailedPermanently(job: Job, error: Exception) {
|
||||
override fun handleJobFailedPermanently(job: Job, dispatcherName: String, error: Exception) {
|
||||
val jobId = job.id ?: return
|
||||
handleJobFailedPermanently(jobId)
|
||||
Log.d(dispatcherName, "permanentlyFailedJob: ${javaClass.simpleName} (id: ${job.id})")
|
||||
}
|
||||
|
||||
private fun handleJobFailedPermanently(jobId: String) {
|
||||
|
@@ -3,6 +3,7 @@ package org.session.libsession.messaging.jobs
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.deferred
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.messages.Message
|
||||
import org.session.libsession.messaging.sending_receiving.MessageReceiver
|
||||
import org.session.libsession.messaging.sending_receiving.handle
|
||||
import org.session.libsession.messaging.utilities.Data
|
||||
@@ -25,46 +26,48 @@ class MessageReceiveJob(val data: ByteArray, val serverHash: String? = null, val
|
||||
private val OPEN_GROUP_ID_KEY = "open_group_id"
|
||||
}
|
||||
|
||||
override fun execute() {
|
||||
executeAsync().get()
|
||||
override suspend fun execute(dispatcherName: String) {
|
||||
executeAsync(dispatcherName).get()
|
||||
}
|
||||
|
||||
fun executeAsync(): Promise<Unit, Exception> {
|
||||
fun executeAsync(dispatcherName: String): Promise<Unit, Exception> {
|
||||
val deferred = deferred<Unit, Exception>()
|
||||
try {
|
||||
val isRetry: Boolean = failureCount != 0
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val serverPublicKey = openGroupID?.let {
|
||||
MessagingModuleConfiguration.shared.storage.getOpenGroupPublicKey(it.split(".").dropLast(1).joinToString("."))
|
||||
storage.getOpenGroupPublicKey(it.split(".").dropLast(1).joinToString("."))
|
||||
}
|
||||
val (message, proto) = MessageReceiver.parse(this.data, this.openGroupMessageServerID, openGroupPublicKey = serverPublicKey)
|
||||
val currentClosedGroups = storage.getAllActiveClosedGroupPublicKeys()
|
||||
val (message, proto) = MessageReceiver.parse(this.data, this.openGroupMessageServerID, openGroupPublicKey = serverPublicKey, currentClosedGroups = currentClosedGroups)
|
||||
val threadId = Message.getThreadId(message, this.openGroupID, storage, false)
|
||||
message.serverHash = serverHash
|
||||
MessageReceiver.handle(message, proto, this.openGroupID)
|
||||
this.handleSuccess()
|
||||
MessageReceiver.handle(message, proto, threadId ?: -1, this.openGroupID)
|
||||
this.handleSuccess(dispatcherName)
|
||||
deferred.resolve(Unit)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Couldn't receive message.", e)
|
||||
if (e is MessageReceiver.Error && !e.isRetryable) {
|
||||
Log.e("Loki", "Message receive job permanently failed.", e)
|
||||
this.handlePermanentFailure(e)
|
||||
this.handlePermanentFailure(dispatcherName, e)
|
||||
} else {
|
||||
Log.e("Loki", "Couldn't receive message.", e)
|
||||
this.handleFailure(e)
|
||||
this.handleFailure(dispatcherName, e)
|
||||
}
|
||||
deferred.resolve(Unit) // The promise is just used to keep track of when we're done
|
||||
}
|
||||
return deferred.promise
|
||||
}
|
||||
|
||||
private fun handleSuccess() {
|
||||
delegate?.handleJobSucceeded(this)
|
||||
private fun handleSuccess(dispatcherName: String) {
|
||||
delegate?.handleJobSucceeded(this, dispatcherName)
|
||||
}
|
||||
|
||||
private fun handlePermanentFailure(e: Exception) {
|
||||
delegate?.handleJobFailedPermanently(this, e)
|
||||
private fun handlePermanentFailure(dispatcherName: String, e: Exception) {
|
||||
delegate?.handleJobFailedPermanently(this, dispatcherName, e)
|
||||
}
|
||||
|
||||
private fun handleFailure(e: Exception) {
|
||||
delegate?.handleJobFailed(this, e)
|
||||
private fun handleFailure(dispatcherName: String, e: Exception) {
|
||||
delegate?.handleJobFailed(this, dispatcherName, e)
|
||||
}
|
||||
|
||||
override fun serialize(): Data {
|
||||
|
@@ -10,7 +10,7 @@ import org.session.libsession.messaging.messages.Message
|
||||
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
||||
import org.session.libsession.messaging.sending_receiving.MessageSender
|
||||
import org.session.libsession.messaging.utilities.Data
|
||||
import org.session.libsession.snode.OnionRequestAPI
|
||||
import org.session.libsignal.utilities.HTTP
|
||||
import org.session.libsignal.utilities.Log
|
||||
|
||||
class MessageSendJob(val message: Message, val destination: Destination) : Job {
|
||||
@@ -32,7 +32,7 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job {
|
||||
private val DESTINATION_KEY = "destination"
|
||||
}
|
||||
|
||||
override fun execute() {
|
||||
override suspend fun execute(dispatcherName: String) {
|
||||
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
|
||||
val message = message as? VisibleMessage
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
@@ -60,21 +60,33 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job {
|
||||
}
|
||||
}
|
||||
if (attachmentsToUpload.isNotEmpty()) {
|
||||
this.handleFailure(AwaitingAttachmentUploadException)
|
||||
this.handleFailure(dispatcherName, AwaitingAttachmentUploadException)
|
||||
return
|
||||
} // Wait for all attachments to upload before continuing
|
||||
}
|
||||
val promise = MessageSender.send(this.message, this.destination).success {
|
||||
this.handleSuccess()
|
||||
val isSync = destination is Destination.Contact && destination.publicKey == sender
|
||||
val promise = MessageSender.send(this.message, this.destination, isSync).success {
|
||||
this.handleSuccess(dispatcherName)
|
||||
}.fail { exception ->
|
||||
Log.e(TAG, "Couldn't send message due to error: $exception.")
|
||||
if (exception is MessageSender.Error) {
|
||||
if (!exception.isRetryable) { this.handlePermanentFailure(exception) }
|
||||
var logStacktrace = true
|
||||
|
||||
when (exception) {
|
||||
// No need for the stack trace for HTTP errors
|
||||
is HTTP.HTTPRequestFailedException -> {
|
||||
logStacktrace = false
|
||||
|
||||
if (exception.statusCode == 429) { this.handlePermanentFailure(dispatcherName, exception) }
|
||||
else { this.handleFailure(dispatcherName, exception) }
|
||||
}
|
||||
is MessageSender.Error -> {
|
||||
if (!exception.isRetryable) { this.handlePermanentFailure(dispatcherName, exception) }
|
||||
else { this.handleFailure(dispatcherName, exception) }
|
||||
}
|
||||
else -> this.handleFailure(dispatcherName, exception)
|
||||
}
|
||||
if (exception is OnionRequestAPI.HTTPRequestFailedAtDestinationException && exception.statusCode == 429) {
|
||||
this.handlePermanentFailure(exception)
|
||||
}
|
||||
this.handleFailure(exception)
|
||||
|
||||
if (logStacktrace) { Log.e(TAG, "Couldn't send message due to error", exception) }
|
||||
else { Log.e(TAG, "Couldn't send message due to error: ${exception.message}") }
|
||||
}
|
||||
try {
|
||||
promise.get()
|
||||
@@ -83,15 +95,15 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job {
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSuccess() {
|
||||
delegate?.handleJobSucceeded(this)
|
||||
private fun handleSuccess(dispatcherName: String) {
|
||||
delegate?.handleJobSucceeded(this, dispatcherName)
|
||||
}
|
||||
|
||||
private fun handlePermanentFailure(error: Exception) {
|
||||
delegate?.handleJobFailedPermanently(this, error)
|
||||
private fun handlePermanentFailure(dispatcherName: String, error: Exception) {
|
||||
delegate?.handleJobFailedPermanently(this, dispatcherName, error)
|
||||
}
|
||||
|
||||
private fun handleFailure(error: Exception) {
|
||||
private fun handleFailure(dispatcherName: String, error: Exception) {
|
||||
Log.w(TAG, "Failed to send $message::class.simpleName.")
|
||||
val message = message as? VisibleMessage
|
||||
if (message != null) {
|
||||
@@ -99,7 +111,7 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job {
|
||||
return // The message has been deleted
|
||||
}
|
||||
}
|
||||
delegate?.handleJobFailed(this, error)
|
||||
delegate?.handleJobFailed(this, dispatcherName, error)
|
||||
}
|
||||
|
||||
override fun serialize(): Data {
|
||||
|
@@ -3,20 +3,17 @@ package org.session.libsession.messaging.jobs
|
||||
import com.esotericsoftware.kryo.Kryo
|
||||
import com.esotericsoftware.kryo.io.Input
|
||||
import com.esotericsoftware.kryo.io.Output
|
||||
import nl.komponents.kovenant.functional.map
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody
|
||||
import org.session.libsession.messaging.jobs.Job.Companion.MAX_BUFFER_SIZE
|
||||
|
||||
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI
|
||||
import org.session.libsession.messaging.sending_receiving.notifications.Server
|
||||
import org.session.libsession.messaging.utilities.Data
|
||||
import org.session.libsession.snode.SnodeMessage
|
||||
import org.session.libsession.snode.OnionRequestAPI
|
||||
import org.session.libsession.snode.SnodeMessage
|
||||
import org.session.libsession.snode.Version
|
||||
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.session.libsignal.utilities.JsonUtil
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.session.libsignal.utilities.retryIfNeeded
|
||||
|
||||
class NotifyPNServerJob(val message: SnodeMessage) : Job {
|
||||
@@ -32,34 +29,38 @@ class NotifyPNServerJob(val message: SnodeMessage) : Job {
|
||||
private val MESSAGE_KEY = "message"
|
||||
}
|
||||
|
||||
override fun execute() {
|
||||
val server = PushNotificationAPI.server
|
||||
override suspend fun execute(dispatcherName: String) {
|
||||
val server = Server.LEGACY
|
||||
val parameters = mapOf( "data" to message.data, "send_to" to message.recipient )
|
||||
val url = "${server}/notify"
|
||||
val url = "${server.url}/notify"
|
||||
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
|
||||
val request = Request.Builder().url(url).post(body)
|
||||
val request = Request.Builder().url(url).post(body).build()
|
||||
retryIfNeeded(4) {
|
||||
OnionRequestAPI.sendOnionRequest(request.build(), server, PushNotificationAPI.serverPublicKey, Version.V2).map { response ->
|
||||
val code = response.info["code"] as? Int
|
||||
if (code == null || code == 0) {
|
||||
Log.d("Loki", "Couldn't notify PN server due to error: ${response.info["message"] as? String ?: "null"}.")
|
||||
OnionRequestAPI.sendOnionRequest(
|
||||
request,
|
||||
server.url,
|
||||
server.publicKey,
|
||||
Version.V2
|
||||
) success { response ->
|
||||
when (response.code) {
|
||||
null, 0 -> Log.d("NotifyPNServerJob", "Couldn't notify PN server due to error: ${response.message}.")
|
||||
}
|
||||
}.fail { exception ->
|
||||
Log.d("Loki", "Couldn't notify PN server due to error: $exception.")
|
||||
} fail { exception ->
|
||||
Log.d("NotifyPNServerJob", "Couldn't notify PN server due to error: $exception.")
|
||||
}
|
||||
}.success {
|
||||
handleSuccess()
|
||||
}. fail {
|
||||
handleFailure(it)
|
||||
} success {
|
||||
handleSuccess(dispatcherName)
|
||||
} fail {
|
||||
handleFailure(dispatcherName, it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSuccess() {
|
||||
delegate?.handleJobSucceeded(this)
|
||||
private fun handleSuccess(dispatcherName: String) {
|
||||
delegate?.handleJobSucceeded(this, dispatcherName)
|
||||
}
|
||||
|
||||
private fun handleFailure(error: Exception) {
|
||||
delegate?.handleJobFailed(this, error)
|
||||
private fun handleFailure(dispatcherName: String, error: Exception) {
|
||||
delegate?.handleJobFailed(this, dispatcherName, error)
|
||||
}
|
||||
|
||||
override fun serialize(): Data {
|
||||
|
@@ -19,18 +19,32 @@ class OpenGroupDeleteJob(private val messageServerIds: LongArray, private val th
|
||||
override var failureCount: Int = 0
|
||||
override val maxFailureCount: Int = 1
|
||||
|
||||
override fun execute() {
|
||||
override suspend fun execute(dispatcherName: String) {
|
||||
val dataProvider = MessagingModuleConfiguration.shared.messageDataProvider
|
||||
val numberToDelete = messageServerIds.size
|
||||
Log.d(TAG, "Deleting $numberToDelete messages")
|
||||
var numberDeleted = 0
|
||||
messageServerIds.forEach { serverId ->
|
||||
val (messageId, isSms) = dataProvider.getMessageID(serverId, threadId) ?: return@forEach
|
||||
dataProvider.deleteMessage(messageId, isSms)
|
||||
numberDeleted++
|
||||
Log.d(TAG, "About to attempt to delete $numberToDelete messages")
|
||||
|
||||
// FIXME: This entire process should probably run in a transaction (with the attachment deletion happening only if it succeeded)
|
||||
try {
|
||||
val messageIds = dataProvider.getMessageIDs(messageServerIds.toList(), threadId)
|
||||
|
||||
// Delete the SMS messages
|
||||
if (messageIds.first.isNotEmpty()) {
|
||||
dataProvider.deleteMessages(messageIds.first, threadId, true)
|
||||
}
|
||||
|
||||
// Delete the MMS messages
|
||||
if (messageIds.second.isNotEmpty()) {
|
||||
dataProvider.deleteMessages(messageIds.second, threadId, false)
|
||||
}
|
||||
|
||||
Log.d(TAG, "Deleted ${messageIds.first.size + messageIds.second.size} messages successfully")
|
||||
delegate?.handleJobSucceeded(this, dispatcherName)
|
||||
}
|
||||
catch (e: Exception) {
|
||||
Log.w(TAG, "OpenGroupDeleteJob failed: $e")
|
||||
delegate?.handleJobFailed(this, dispatcherName, e)
|
||||
}
|
||||
Log.d(TAG, "Deleted $numberDeleted messages successfully")
|
||||
delegate?.handleJobSucceeded(this)
|
||||
}
|
||||
|
||||
override fun serialize(): Data = Data.Builder()
|
||||
|
@@ -0,0 +1,121 @@
|
||||
package org.session.libsession.messaging.jobs
|
||||
|
||||
import org.session.libsession.avatars.AvatarHelper
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.utilities.Data
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.DownloadUtilities.downloadFile
|
||||
import org.session.libsession.utilities.TextSecurePreferences.Companion.setProfileAvatarId
|
||||
import org.session.libsession.utilities.TextSecurePreferences.Companion.setProfilePictureURL
|
||||
import org.session.libsession.utilities.Util.copy
|
||||
import org.session.libsession.utilities.Util.equals
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.session.libsignal.streams.ProfileCipherInputStream
|
||||
import org.session.libsignal.utilities.Log
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.io.InputStream
|
||||
import java.security.SecureRandom
|
||||
import java.util.concurrent.ConcurrentSkipListSet
|
||||
|
||||
class RetrieveProfileAvatarJob(private val profileAvatar: String?, val recipientAddress: Address): Job {
|
||||
override var delegate: JobDelegate? = null
|
||||
override var id: String? = null
|
||||
override var failureCount: Int = 0
|
||||
override val maxFailureCount: Int = 3
|
||||
|
||||
companion object {
|
||||
val TAG = RetrieveProfileAvatarJob::class.simpleName
|
||||
val KEY: String = "RetrieveProfileAvatarJob"
|
||||
|
||||
// Keys used for database storage
|
||||
private const val PROFILE_AVATAR_KEY = "profileAvatar"
|
||||
private const val RECEIPIENT_ADDRESS_KEY = "recipient"
|
||||
|
||||
val errorUrls = ConcurrentSkipListSet<String>()
|
||||
|
||||
}
|
||||
|
||||
override suspend fun execute(dispatcherName: String) {
|
||||
val delegate = delegate ?: return
|
||||
if (profileAvatar in errorUrls) return delegate.handleJobFailed(this, dispatcherName, Exception("Profile URL 404'd this app instance"))
|
||||
val context = MessagingModuleConfiguration.shared.context
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val recipient = Recipient.from(context, recipientAddress, true)
|
||||
val profileKey = recipient.resolve().profileKey
|
||||
|
||||
if (profileKey == null || (profileKey.size != 32 && profileKey.size != 16)) {
|
||||
return delegate.handleJobFailedPermanently(this, dispatcherName, Exception("Recipient profile key is gone!"))
|
||||
}
|
||||
|
||||
// Commit '78d1e9d' (fix: open group threads and avatar downloads) had this commented out so
|
||||
// it's now limited to just the current user case
|
||||
if (
|
||||
recipient.isLocalNumber &&
|
||||
AvatarHelper.avatarFileExists(context, recipient.resolve().address) &&
|
||||
equals(profileAvatar, recipient.resolve().profileAvatar)
|
||||
) {
|
||||
Log.w(TAG, "Already retrieved profile avatar: $profileAvatar")
|
||||
return
|
||||
}
|
||||
|
||||
if (profileAvatar.isNullOrEmpty()) {
|
||||
Log.w(TAG, "Removing profile avatar for: " + recipient.address.serialize())
|
||||
|
||||
if (recipient.isLocalNumber) {
|
||||
setProfileAvatarId(context, SecureRandom().nextInt())
|
||||
setProfilePictureURL(context, null)
|
||||
}
|
||||
|
||||
AvatarHelper.delete(context, recipient.address)
|
||||
storage.setProfileAvatar(recipient, null)
|
||||
return
|
||||
}
|
||||
|
||||
val downloadDestination = File.createTempFile("avatar", ".jpg", context.cacheDir)
|
||||
|
||||
try {
|
||||
downloadFile(downloadDestination, profileAvatar)
|
||||
val avatarStream: InputStream = ProfileCipherInputStream(FileInputStream(downloadDestination), profileKey)
|
||||
val decryptDestination = File.createTempFile("avatar", ".jpg", context.cacheDir)
|
||||
copy(avatarStream, FileOutputStream(decryptDestination))
|
||||
decryptDestination.renameTo(AvatarHelper.getAvatarFile(context, recipient.address))
|
||||
|
||||
if (recipient.isLocalNumber) {
|
||||
setProfileAvatarId(context, SecureRandom().nextInt())
|
||||
setProfilePictureURL(context, profileAvatar)
|
||||
}
|
||||
|
||||
storage.setProfileAvatar(recipient, profileAvatar)
|
||||
} catch (e: Exception) {
|
||||
Log.e("Loki", "Failed to download profile avatar", e)
|
||||
if (failureCount + 1 >= maxFailureCount) {
|
||||
errorUrls += profileAvatar
|
||||
}
|
||||
return delegate.handleJobFailed(this, dispatcherName, e)
|
||||
} finally {
|
||||
downloadDestination.delete()
|
||||
}
|
||||
return delegate.handleJobSucceeded(this, dispatcherName)
|
||||
}
|
||||
|
||||
override fun serialize(): Data {
|
||||
return Data.Builder()
|
||||
.putString(PROFILE_AVATAR_KEY, profileAvatar)
|
||||
.putString(RECEIPIENT_ADDRESS_KEY, recipientAddress.serialize())
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun getFactoryKey(): String {
|
||||
return KEY
|
||||
}
|
||||
|
||||
class Factory: Job.Factory<RetrieveProfileAvatarJob> {
|
||||
override fun create(data: Data): RetrieveProfileAvatarJob {
|
||||
val profileAvatar = if (data.hasString(PROFILE_AVATAR_KEY)) { data.getString(PROFILE_AVATAR_KEY) } else { null }
|
||||
val recipientAddress = Address.fromSerialized(data.getString(RECEIPIENT_ADDRESS_KEY))
|
||||
return RetrieveProfileAvatarJob(profileAvatar, recipientAddress)
|
||||
}
|
||||
}
|
||||
}
|
@@ -16,6 +16,7 @@ class SessionJobManagerFactories {
|
||||
GroupAvatarDownloadJob.KEY to GroupAvatarDownloadJob.Factory(),
|
||||
BackgroundGroupAddJob.KEY to BackgroundGroupAddJob.Factory(),
|
||||
OpenGroupDeleteJob.KEY to OpenGroupDeleteJob.Factory(),
|
||||
ConfigurationSyncJob.KEY to ConfigurationSyncJob.Factory()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@@ -20,7 +20,7 @@ class TrimThreadJob(val threadId: Long, val openGroupId: String?) : Job {
|
||||
const val THREAD_LENGTH_TRIGGER_SIZE = 2000
|
||||
}
|
||||
|
||||
override fun execute() {
|
||||
override suspend fun execute(dispatcherName: String) {
|
||||
val context = MessagingModuleConfiguration.shared.context
|
||||
val trimmingEnabled = TextSecurePreferences.isThreadLengthTrimmingEnabled(context)
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
@@ -29,7 +29,7 @@ class TrimThreadJob(val threadId: Long, val openGroupId: String?) : Job {
|
||||
val oldestMessageTime = System.currentTimeMillis() - TRIM_TIME_LIMIT
|
||||
storage.trimThreadBefore(threadId, oldestMessageTime)
|
||||
}
|
||||
delegate?.handleJobSucceeded(this)
|
||||
delegate?.handleJobSucceeded(this, dispatcherName)
|
||||
}
|
||||
|
||||
override fun serialize(): Data {
|
||||
|
@@ -2,6 +2,7 @@ package org.session.libsession.messaging.mentions
|
||||
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.contacts.Contact
|
||||
import java.util.Locale
|
||||
|
||||
object MentionsManager {
|
||||
var userPublicKeyCache = mutableMapOf<Long, Set<String>>() // Thread ID to set of user hex encoded public keys
|
||||
@@ -32,9 +33,9 @@ object MentionsManager {
|
||||
candidates.sortedBy { it.displayName }
|
||||
if (query.length >= 2) {
|
||||
// Filter out any non-matching candidates
|
||||
candidates = candidates.filter { it.displayName.toLowerCase().contains(query.toLowerCase()) }
|
||||
candidates = candidates.filter { it.displayName.lowercase(Locale.getDefault()).contains(query.lowercase(Locale.getDefault())) }
|
||||
// Sort based on where in the candidate the query occurs
|
||||
candidates.sortedBy { it.displayName.toLowerCase().indexOf(query.toLowerCase()) }
|
||||
candidates.sortedBy { it.displayName.lowercase(Locale.getDefault()).indexOf(query.lowercase(Locale.getDefault())) }
|
||||
}
|
||||
// Return
|
||||
return candidates
|
||||
|
@@ -7,13 +7,13 @@ import org.session.libsignal.utilities.toHexString
|
||||
|
||||
sealed class Destination {
|
||||
|
||||
class Contact(var publicKey: String) : Destination() {
|
||||
data class Contact(var publicKey: String) : Destination() {
|
||||
internal constructor(): this("")
|
||||
}
|
||||
class ClosedGroup(var groupPublicKey: String) : Destination() {
|
||||
data class ClosedGroup(var groupPublicKey: String) : Destination() {
|
||||
internal constructor(): this("")
|
||||
}
|
||||
class LegacyOpenGroup(var roomToken: String, var server: String) : Destination() {
|
||||
data class LegacyOpenGroup(var roomToken: String, var server: String) : Destination() {
|
||||
internal constructor(): this("", "")
|
||||
}
|
||||
|
||||
@@ -43,14 +43,14 @@ sealed class Destination {
|
||||
val groupPublicKey = GroupUtil.doubleDecodeGroupID(groupID).toHexString()
|
||||
ClosedGroup(groupPublicKey)
|
||||
}
|
||||
address.isOpenGroup -> {
|
||||
address.isCommunity -> {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val threadID = storage.getThreadId(address)!!
|
||||
storage.getOpenGroup(threadID)?.let {
|
||||
OpenGroup(roomToken = it.room, server = it.server, fileIds = fileIds)
|
||||
} ?: throw Exception("Missing open group for thread with ID: $threadID.")
|
||||
}
|
||||
address.isOpenGroupInbox -> {
|
||||
address.isCommunityInbox -> {
|
||||
val groupInboxId = GroupUtil.getDecodedGroupID(address.serialize()).split("!")
|
||||
OpenGroupInbox(
|
||||
groupInboxId.dropLast(2).joinToString("!"),
|
||||
|
@@ -0,0 +1,20 @@
|
||||
package org.session.libsession.messaging.messages
|
||||
|
||||
import network.loki.messenger.libsession_util.util.ExpiryMode
|
||||
|
||||
data class ExpirationConfiguration(
|
||||
val threadId: Long = -1,
|
||||
val expiryMode: ExpiryMode = ExpiryMode.NONE,
|
||||
val updatedTimestampMs: Long = 0
|
||||
) {
|
||||
val isEnabled = expiryMode.expirySeconds > 0
|
||||
|
||||
companion object {
|
||||
val isNewConfigEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
data class ExpirationDatabaseMetadata(
|
||||
val threadId: Long = -1,
|
||||
val updatedTimestampMs: Long
|
||||
)
|
@@ -1,8 +1,14 @@
|
||||
package org.session.libsession.messaging.messages
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import network.loki.messenger.libsession_util.util.ExpiryMode
|
||||
import org.session.libsession.database.StorageProtocol
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate
|
||||
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsignal.protos.SignalServiceProtos
|
||||
import org.session.libsignal.protos.SignalServiceProtos.Content.ExpirationType
|
||||
|
||||
abstract class Message {
|
||||
var id: Long? = null
|
||||
@@ -11,29 +17,79 @@ abstract class Message {
|
||||
var receivedTimestamp: Long? = null
|
||||
var recipient: String? = null
|
||||
var sender: String? = null
|
||||
var isSenderSelf: Boolean = false
|
||||
var groupPublicKey: String? = null
|
||||
var openGroupServerMessageID: Long? = null
|
||||
var serverHash: String? = null
|
||||
var specifiedTtl: Long? = null
|
||||
|
||||
open val ttl: Long = 14 * 24 * 60 * 60 * 1000
|
||||
var expiryMode: ExpiryMode = ExpiryMode.NONE
|
||||
|
||||
open val coerceDisappearAfterSendToRead = false
|
||||
|
||||
open val defaultTtl: Long = 14 * 24 * 60 * 60 * 1000
|
||||
open val ttl: Long get() = specifiedTtl ?: defaultTtl
|
||||
open val isSelfSendValid: Boolean = false
|
||||
|
||||
open fun isValid(): Boolean {
|
||||
val sentTimestamp = sentTimestamp
|
||||
if (sentTimestamp != null && sentTimestamp <= 0) { return false }
|
||||
val receivedTimestamp = receivedTimestamp
|
||||
if (receivedTimestamp != null && receivedTimestamp <= 0) { return false }
|
||||
return sender != null && recipient != null
|
||||
companion object {
|
||||
fun getThreadId(message: Message, openGroupID: String?, storage: StorageProtocol, shouldCreateThread: Boolean): Long? {
|
||||
val senderOrSync = when (message) {
|
||||
is VisibleMessage -> message.syncTarget ?: message.sender!!
|
||||
is ExpirationTimerUpdate -> message.syncTarget ?: message.sender!!
|
||||
else -> message.sender!!
|
||||
}
|
||||
return storage.getThreadIdFor(senderOrSync, message.groupPublicKey, openGroupID, createThread = shouldCreateThread)
|
||||
}
|
||||
}
|
||||
|
||||
open fun isValid(): Boolean =
|
||||
sentTimestamp?.let { it > 0 } != false
|
||||
&& receivedTimestamp?.let { it > 0 } != false
|
||||
&& sender != null
|
||||
&& recipient != null
|
||||
|
||||
abstract fun toProto(): SignalServiceProtos.Content?
|
||||
|
||||
fun setGroupContext(dataMessage: SignalServiceProtos.DataMessage.Builder) {
|
||||
val groupProto = SignalServiceProtos.GroupContext.newBuilder()
|
||||
val groupID = GroupUtil.doubleEncodeGroupID(recipient!!)
|
||||
groupProto.id = ByteString.copyFrom(GroupUtil.getDecodedGroupIDAsData(groupID))
|
||||
groupProto.type = SignalServiceProtos.GroupContext.Type.DELIVER
|
||||
dataMessage.group = groupProto.build()
|
||||
fun SignalServiceProtos.DataMessage.Builder.setGroupContext() {
|
||||
group = SignalServiceProtos.GroupContext.newBuilder().apply {
|
||||
id = GroupUtil.doubleEncodeGroupID(recipient!!).let(GroupUtil::getDecodedGroupIDAsData).let(ByteString::copyFrom)
|
||||
type = SignalServiceProtos.GroupContext.Type.DELIVER
|
||||
}.build()
|
||||
}
|
||||
|
||||
}
|
||||
fun SignalServiceProtos.Content.Builder.applyExpiryMode() = apply {
|
||||
expirationTimer = expiryMode.expirySeconds.toInt()
|
||||
expirationType = when (expiryMode) {
|
||||
is ExpiryMode.AfterSend -> ExpirationType.DELETE_AFTER_SEND
|
||||
is ExpiryMode.AfterRead -> ExpirationType.DELETE_AFTER_READ
|
||||
else -> ExpirationType.UNKNOWN
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <reified M: Message> M.copyExpiration(proto: SignalServiceProtos.Content): M = apply {
|
||||
(proto.takeIf { it.hasExpirationTimer() }?.expirationTimer ?: proto.dataMessage?.expireTimer)?.let { duration ->
|
||||
expiryMode = when (proto.expirationType.takeIf { duration > 0 }) {
|
||||
ExpirationType.DELETE_AFTER_SEND -> ExpiryMode.AfterSend(duration.toLong())
|
||||
ExpirationType.DELETE_AFTER_READ -> ExpiryMode.AfterRead(duration.toLong())
|
||||
else -> ExpiryMode.NONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun SignalServiceProtos.Content.expiryMode(): ExpiryMode =
|
||||
(takeIf { it.hasExpirationTimer() }?.expirationTimer ?: dataMessage?.expireTimer)?.let { duration ->
|
||||
when (expirationType.takeIf { duration > 0 }) {
|
||||
ExpirationType.DELETE_AFTER_SEND -> ExpiryMode.AfterSend(duration.toLong())
|
||||
ExpirationType.DELETE_AFTER_READ -> ExpiryMode.AfterRead(duration.toLong())
|
||||
else -> ExpiryMode.NONE
|
||||
}
|
||||
} ?: ExpiryMode.NONE
|
||||
|
||||
/**
|
||||
* Apply ExpiryMode from the current setting.
|
||||
*/
|
||||
inline fun <reified M: Message> M.applyExpiryMode(thread: Long): M = apply {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
expiryMode = storage.getExpirationConfiguration(thread)?.expiryMode?.coerceSendToRead(coerceDisappearAfterSendToRead) ?: ExpiryMode.NONE
|
||||
}
|
||||
|
@@ -1,5 +1,7 @@
|
||||
package org.session.libsession.messaging.messages.control
|
||||
|
||||
import org.session.libsession.messaging.messages.applyExpiryMode
|
||||
import org.session.libsession.messaging.messages.copyExpiration
|
||||
import org.session.libsignal.protos.SignalServiceProtos
|
||||
import org.session.libsignal.protos.SignalServiceProtos.CallMessage.Type.*
|
||||
import org.session.libsignal.utilities.Log
|
||||
@@ -12,12 +14,13 @@ class CallMessage(): ControlMessage() {
|
||||
var sdpMids: List<String> = listOf()
|
||||
var callId: UUID? = null
|
||||
|
||||
override val coerceDisappearAfterSendToRead = true
|
||||
override val isSelfSendValid: Boolean get() = type in arrayOf(ANSWER, END_CALL)
|
||||
|
||||
override val ttl: Long = 300000L // 5m
|
||||
override val defaultTtl: Long = 300000L // 5m
|
||||
|
||||
override fun isValid(): Boolean = super.isValid() && type != null && callId != null
|
||||
&& (!sdps.isNullOrEmpty() || type in listOf(END_CALL, PRE_OFFER))
|
||||
&& (sdps.isNotEmpty() || type in listOf(END_CALL, PRE_OFFER))
|
||||
|
||||
constructor(type: SignalServiceProtos.CallMessage.Type,
|
||||
sdps: List<String>,
|
||||
@@ -64,7 +67,8 @@ class CallMessage(): ControlMessage() {
|
||||
val sdpMLineIndexes = callMessageProto.sdpMLineIndexesList
|
||||
val sdpMids = callMessageProto.sdpMidsList
|
||||
val callId = UUID.fromString(callMessageProto.uuid)
|
||||
return CallMessage(type,sdps, sdpMLineIndexes, sdpMids, callId)
|
||||
return CallMessage(type, sdps, sdpMLineIndexes, sdpMids, callId)
|
||||
.copyExpiration(proto)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,9 +86,8 @@ class CallMessage(): ControlMessage() {
|
||||
.setUuid(callId!!.toString())
|
||||
|
||||
return SignalServiceProtos.Content.newBuilder()
|
||||
.setCallMessage(
|
||||
callMessage
|
||||
)
|
||||
.applyExpiryMode()
|
||||
.setCallMessage(callMessage)
|
||||
.build()
|
||||
}
|
||||
|
||||
|
@@ -6,15 +6,22 @@ import org.session.libsignal.crypto.ecc.DjbECPublicKey
|
||||
import org.session.libsignal.crypto.ecc.ECKeyPair
|
||||
import org.session.libsignal.protos.SignalServiceProtos
|
||||
import org.session.libsignal.protos.SignalServiceProtos.DataMessage
|
||||
import org.session.libsignal.utilities.removingIdPrefixIfNeeded
|
||||
import org.session.libsignal.utilities.toHexString
|
||||
import org.session.libsignal.protos.SignalServiceProtos.DataMessage.ClosedGroupControlMessage.Type.ENCRYPTION_KEY_PAIR
|
||||
import org.session.libsignal.protos.SignalServiceProtos.DataMessage.ClosedGroupControlMessage.Type.MEMBERS_ADDED
|
||||
import org.session.libsignal.protos.SignalServiceProtos.DataMessage.ClosedGroupControlMessage.Type.MEMBERS_REMOVED
|
||||
import org.session.libsignal.protos.SignalServiceProtos.DataMessage.ClosedGroupControlMessage.Type.MEMBER_LEFT
|
||||
import org.session.libsignal.protos.SignalServiceProtos.DataMessage.ClosedGroupControlMessage.Type.NAME_CHANGE
|
||||
import org.session.libsignal.protos.SignalServiceProtos.DataMessage.ClosedGroupControlMessage.Type.NEW
|
||||
import org.session.libsignal.utilities.Hex
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.session.libsignal.utilities.removingIdPrefixIfNeeded
|
||||
import org.session.libsignal.utilities.toHexString
|
||||
|
||||
class ClosedGroupControlMessage() : ControlMessage() {
|
||||
var kind: Kind? = null
|
||||
var groupID: String? = null
|
||||
|
||||
override val ttl: Long get() {
|
||||
override val defaultTtl: Long get() {
|
||||
return when (kind) {
|
||||
is Kind.EncryptionKeyPair -> 14 * 24 * 60 * 60 * 1000
|
||||
else -> 14 * 24 * 60 * 60 * 1000
|
||||
@@ -76,50 +83,30 @@ class ClosedGroupControlMessage() : ControlMessage() {
|
||||
companion object {
|
||||
const val TAG = "ClosedGroupControlMessage"
|
||||
|
||||
fun fromProto(proto: SignalServiceProtos.Content): ClosedGroupControlMessage? {
|
||||
if (!proto.hasDataMessage() || !proto.dataMessage.hasClosedGroupControlMessage()) return null
|
||||
val closedGroupControlMessageProto = proto.dataMessage!!.closedGroupControlMessage!!
|
||||
val kind: Kind
|
||||
when (closedGroupControlMessageProto.type!!) {
|
||||
DataMessage.ClosedGroupControlMessage.Type.NEW -> {
|
||||
val publicKey = closedGroupControlMessageProto.publicKey ?: return null
|
||||
val name = closedGroupControlMessageProto.name ?: return null
|
||||
val encryptionKeyPairAsProto = closedGroupControlMessageProto.encryptionKeyPair ?: return null
|
||||
val expirationTimer = closedGroupControlMessageProto.expirationTimer
|
||||
try {
|
||||
val encryptionKeyPair = ECKeyPair(DjbECPublicKey(encryptionKeyPairAsProto.publicKey.toByteArray()),
|
||||
DjbECPrivateKey(encryptionKeyPairAsProto.privateKey.toByteArray()))
|
||||
kind = Kind.New(publicKey, name, encryptionKeyPair, closedGroupControlMessageProto.membersList, closedGroupControlMessageProto.adminsList, expirationTimer)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Couldn't parse key pair from proto: $encryptionKeyPairAsProto.")
|
||||
return null
|
||||
}
|
||||
}
|
||||
DataMessage.ClosedGroupControlMessage.Type.ENCRYPTION_KEY_PAIR -> {
|
||||
val publicKey = closedGroupControlMessageProto.publicKey
|
||||
val wrappers = closedGroupControlMessageProto.wrappersList.mapNotNull { KeyPairWrapper.fromProto(it) }
|
||||
kind = Kind.EncryptionKeyPair(publicKey, wrappers)
|
||||
}
|
||||
DataMessage.ClosedGroupControlMessage.Type.NAME_CHANGE -> {
|
||||
val name = closedGroupControlMessageProto.name ?: return null
|
||||
kind = Kind.NameChange(name)
|
||||
}
|
||||
DataMessage.ClosedGroupControlMessage.Type.MEMBERS_ADDED -> {
|
||||
kind = Kind.MembersAdded(closedGroupControlMessageProto.membersList)
|
||||
}
|
||||
DataMessage.ClosedGroupControlMessage.Type.MEMBERS_REMOVED -> {
|
||||
kind = Kind.MembersRemoved(closedGroupControlMessageProto.membersList)
|
||||
}
|
||||
DataMessage.ClosedGroupControlMessage.Type.MEMBER_LEFT -> {
|
||||
kind = Kind.MemberLeft()
|
||||
}
|
||||
}
|
||||
return ClosedGroupControlMessage(kind)
|
||||
}
|
||||
fun fromProto(proto: SignalServiceProtos.Content): ClosedGroupControlMessage? =
|
||||
proto.takeIf { it.hasDataMessage() }?.dataMessage
|
||||
?.takeIf { it.hasClosedGroupControlMessage() }?.closedGroupControlMessage
|
||||
?.run {
|
||||
when (type) {
|
||||
NEW -> takeIf { it.hasPublicKey() && it.hasEncryptionKeyPair() && it.hasName() }?.let {
|
||||
ECKeyPair(
|
||||
DjbECPublicKey(encryptionKeyPair.publicKey.toByteArray()),
|
||||
DjbECPrivateKey(encryptionKeyPair.privateKey.toByteArray())
|
||||
).let { Kind.New(publicKey, name, it, membersList, adminsList, expirationTimer) }
|
||||
}
|
||||
ENCRYPTION_KEY_PAIR -> Kind.EncryptionKeyPair(publicKey, wrappersList.mapNotNull(KeyPairWrapper::fromProto))
|
||||
NAME_CHANGE -> takeIf { it.hasName() }?.let { Kind.NameChange(name) }
|
||||
MEMBERS_ADDED -> Kind.MembersAdded(membersList)
|
||||
MEMBERS_REMOVED -> Kind.MembersRemoved(membersList)
|
||||
MEMBER_LEFT -> Kind.MemberLeft()
|
||||
else -> null
|
||||
}?.let(::ClosedGroupControlMessage)
|
||||
}
|
||||
}
|
||||
|
||||
internal constructor(kind: Kind?) : this() {
|
||||
internal constructor(kind: Kind?, groupID: String? = null) : this() {
|
||||
this.kind = kind
|
||||
this.groupID = groupID
|
||||
}
|
||||
|
||||
override fun toProto(): SignalServiceProtos.Content? {
|
||||
@@ -132,45 +119,44 @@ class ClosedGroupControlMessage() : ControlMessage() {
|
||||
val closedGroupControlMessage: DataMessage.ClosedGroupControlMessage.Builder = DataMessage.ClosedGroupControlMessage.newBuilder()
|
||||
when (kind) {
|
||||
is Kind.New -> {
|
||||
closedGroupControlMessage.type = DataMessage.ClosedGroupControlMessage.Type.NEW
|
||||
closedGroupControlMessage.type = NEW
|
||||
closedGroupControlMessage.publicKey = kind.publicKey
|
||||
closedGroupControlMessage.name = kind.name
|
||||
val encryptionKeyPair = SignalServiceProtos.KeyPair.newBuilder()
|
||||
encryptionKeyPair.publicKey = ByteString.copyFrom(kind.encryptionKeyPair!!.publicKey.serialize().removingIdPrefixIfNeeded())
|
||||
encryptionKeyPair.privateKey = ByteString.copyFrom(kind.encryptionKeyPair!!.privateKey.serialize())
|
||||
closedGroupControlMessage.encryptionKeyPair = encryptionKeyPair.build()
|
||||
closedGroupControlMessage.encryptionKeyPair = SignalServiceProtos.KeyPair.newBuilder().also {
|
||||
it.publicKey = ByteString.copyFrom(kind.encryptionKeyPair!!.publicKey.serialize().removingIdPrefixIfNeeded())
|
||||
it.privateKey = ByteString.copyFrom(kind.encryptionKeyPair!!.privateKey.serialize())
|
||||
}.build()
|
||||
closedGroupControlMessage.addAllMembers(kind.members)
|
||||
closedGroupControlMessage.addAllAdmins(kind.admins)
|
||||
closedGroupControlMessage.expirationTimer = kind.expirationTimer
|
||||
}
|
||||
is Kind.EncryptionKeyPair -> {
|
||||
closedGroupControlMessage.type = DataMessage.ClosedGroupControlMessage.Type.ENCRYPTION_KEY_PAIR
|
||||
closedGroupControlMessage.type = ENCRYPTION_KEY_PAIR
|
||||
closedGroupControlMessage.publicKey = kind.publicKey ?: ByteString.EMPTY
|
||||
closedGroupControlMessage.addAllWrappers(kind.wrappers.map { it.toProto() })
|
||||
}
|
||||
is Kind.NameChange -> {
|
||||
closedGroupControlMessage.type = DataMessage.ClosedGroupControlMessage.Type.NAME_CHANGE
|
||||
closedGroupControlMessage.type = NAME_CHANGE
|
||||
closedGroupControlMessage.name = kind.name
|
||||
}
|
||||
is Kind.MembersAdded -> {
|
||||
closedGroupControlMessage.type = DataMessage.ClosedGroupControlMessage.Type.MEMBERS_ADDED
|
||||
closedGroupControlMessage.type = MEMBERS_ADDED
|
||||
closedGroupControlMessage.addAllMembers(kind.members)
|
||||
}
|
||||
is Kind.MembersRemoved -> {
|
||||
closedGroupControlMessage.type = DataMessage.ClosedGroupControlMessage.Type.MEMBERS_REMOVED
|
||||
closedGroupControlMessage.type = MEMBERS_REMOVED
|
||||
closedGroupControlMessage.addAllMembers(kind.members)
|
||||
}
|
||||
is Kind.MemberLeft -> {
|
||||
closedGroupControlMessage.type = DataMessage.ClosedGroupControlMessage.Type.MEMBER_LEFT
|
||||
closedGroupControlMessage.type = MEMBER_LEFT
|
||||
}
|
||||
}
|
||||
val contentProto = SignalServiceProtos.Content.newBuilder()
|
||||
val dataMessageProto = DataMessage.newBuilder()
|
||||
dataMessageProto.closedGroupControlMessage = closedGroupControlMessage.build()
|
||||
// Group context
|
||||
setGroupContext(dataMessageProto)
|
||||
contentProto.dataMessage = dataMessageProto.build()
|
||||
return contentProto.build()
|
||||
return SignalServiceProtos.Content.newBuilder().apply {
|
||||
dataMessage = DataMessage.newBuilder().also {
|
||||
it.closedGroupControlMessage = closedGroupControlMessage.build()
|
||||
it.setGroupContext()
|
||||
}.build()
|
||||
}.build()
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Couldn't construct closed group control message proto from: $this.")
|
||||
return null
|
||||
@@ -191,11 +177,9 @@ class ClosedGroupControlMessage() : ControlMessage() {
|
||||
}
|
||||
|
||||
fun toProto(): DataMessage.ClosedGroupControlMessage.KeyPairWrapper? {
|
||||
val publicKey = publicKey ?: return null
|
||||
val encryptedKeyPair = encryptedKeyPair ?: return null
|
||||
val result = DataMessage.ClosedGroupControlMessage.KeyPairWrapper.newBuilder()
|
||||
result.publicKey = ByteString.copyFrom(Hex.fromStringCondensed(publicKey))
|
||||
result.encryptedKeyPair = encryptedKeyPair
|
||||
result.publicKey = ByteString.copyFrom(Hex.fromStringCondensed(publicKey ?: return null))
|
||||
result.encryptedKeyPair = encryptedKeyPair ?: return null
|
||||
return try {
|
||||
result.build()
|
||||
} catch (e: Exception) {
|
||||
@@ -204,4 +188,4 @@ class ClosedGroupControlMessage() : ControlMessage() {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -2,28 +2,28 @@ package org.session.libsession.messaging.messages.control
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.messages.copyExpiration
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsession.utilities.ProfileKeyUtil
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.crypto.ecc.DjbECPrivateKey
|
||||
import org.session.libsignal.crypto.ecc.DjbECPublicKey
|
||||
import org.session.libsignal.crypto.ecc.ECKeyPair
|
||||
import org.session.libsignal.protos.SignalServiceProtos
|
||||
import org.session.libsignal.utilities.Hex
|
||||
import org.session.libsignal.utilities.removingIdPrefixIfNeeded
|
||||
import org.session.libsignal.utilities.toHexString
|
||||
import org.session.libsignal.utilities.Hex
|
||||
|
||||
class ConfigurationMessage(var closedGroups: List<ClosedGroup>, var openGroups: List<String>, var contacts: List<Contact>,
|
||||
var displayName: String, var profilePicture: String?, var profileKey: ByteArray) : ControlMessage() {
|
||||
|
||||
override val isSelfSendValid: Boolean = true
|
||||
|
||||
class ClosedGroup(var publicKey: String, var name: String, var encryptionKeyPair: ECKeyPair?, var members: List<String>, var admins: List<String>, var expirationTimer: Int) {
|
||||
class ClosedGroup(var publicKey: String, var name: String, var encryptionKeyPair: ECKeyPair?, var members: List<String>, var admins: List<String>) {
|
||||
val isValid: Boolean get() = members.isNotEmpty() && admins.isNotEmpty()
|
||||
|
||||
internal constructor() : this("", "", null, listOf(), listOf(), 0)
|
||||
internal constructor() : this("", "", null, listOf(), listOf())
|
||||
|
||||
override fun toString(): String {
|
||||
return name
|
||||
@@ -40,8 +40,7 @@ class ConfigurationMessage(var closedGroups: List<ClosedGroup>, var openGroups:
|
||||
DjbECPrivateKey(encryptionKeyPairAsProto.privateKey.toByteArray()))
|
||||
val members = proto.membersList.map { it.toByteArray().toHexString() }
|
||||
val admins = proto.adminsList.map { it.toByteArray().toHexString() }
|
||||
val expirationTimer = proto.expirationTimer
|
||||
return ClosedGroup(publicKey, name, encryptionKeyPair, members, admins, expirationTimer)
|
||||
return ClosedGroup(publicKey, name, encryptionKeyPair, members, admins)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +54,6 @@ class ConfigurationMessage(var closedGroups: List<ClosedGroup>, var openGroups:
|
||||
result.encryptionKeyPair = encryptionKeyPairAsProto.build()
|
||||
result.addAllMembers(members.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) })
|
||||
result.addAllAdmins(admins.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) })
|
||||
result.expirationTimer = expirationTimer
|
||||
return result.build()
|
||||
}
|
||||
}
|
||||
@@ -122,14 +120,19 @@ class ConfigurationMessage(var closedGroups: List<ClosedGroup>, var openGroups:
|
||||
val displayName = TextSecurePreferences.getProfileName(context) ?: return null
|
||||
val profilePicture = TextSecurePreferences.getProfilePictureURL(context)
|
||||
val profileKey = ProfileKeyUtil.getProfileKey(context)
|
||||
val groups = storage.getAllGroups()
|
||||
val groups = storage.getAllGroups(includeInactive = false)
|
||||
for (group in groups) {
|
||||
if (group.isClosedGroup) {
|
||||
if (group.isClosedGroup && group.isActive) {
|
||||
if (!group.members.contains(Address.fromSerialized(storage.getUserPublicKey()!!))) continue
|
||||
val groupPublicKey = GroupUtil.doubleDecodeGroupID(group.encodedId).toHexString()
|
||||
val encryptionKeyPair = storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) ?: continue
|
||||
val recipient = Recipient.from(context, Address.fromSerialized(group.encodedId), false)
|
||||
val closedGroup = ClosedGroup(groupPublicKey, group.title, encryptionKeyPair, group.members.map { it.serialize() }, group.admins.map { it.serialize() }, recipient.expireMessages)
|
||||
val closedGroup = ClosedGroup(
|
||||
groupPublicKey,
|
||||
group.title,
|
||||
encryptionKeyPair,
|
||||
group.members.map { it.serialize() },
|
||||
group.admins.map { it.serialize() }
|
||||
)
|
||||
closedGroups.add(closedGroup)
|
||||
}
|
||||
if (group.isOpenGroup) {
|
||||
@@ -152,6 +155,7 @@ class ConfigurationMessage(var closedGroups: List<ClosedGroup>, var openGroups:
|
||||
val profileKey = configurationProto.profileKey
|
||||
val contacts = configurationProto.contactsList.mapNotNull { Contact.fromProto(it) }
|
||||
return ConfigurationMessage(closedGroups, openGroups, contacts, displayName, profilePicture, profileKey.toByteArray())
|
||||
.copyExpiration(proto)
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,11 +1,14 @@
|
||||
package org.session.libsession.messaging.messages.control
|
||||
|
||||
import org.session.libsession.messaging.messages.copyExpiration
|
||||
import org.session.libsignal.protos.SignalServiceProtos
|
||||
import org.session.libsignal.utilities.Log
|
||||
|
||||
class DataExtractionNotification() : ControlMessage() {
|
||||
var kind: Kind? = null
|
||||
|
||||
override val coerceDisappearAfterSendToRead = true
|
||||
|
||||
sealed class Kind {
|
||||
class Screenshot() : Kind()
|
||||
class MediaSaved(val timestamp: Long) : Kind()
|
||||
@@ -31,6 +34,7 @@ class DataExtractionNotification() : ControlMessage() {
|
||||
}
|
||||
}
|
||||
return DataExtractionNotification(kind)
|
||||
.copyExpiration(proto)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,9 +66,10 @@ class DataExtractionNotification() : ControlMessage() {
|
||||
dataExtractionNotification.timestamp = kind.timestamp
|
||||
}
|
||||
}
|
||||
val contentProto = SignalServiceProtos.Content.newBuilder()
|
||||
contentProto.dataExtractionNotification = dataExtractionNotification.build()
|
||||
return contentProto.build()
|
||||
return SignalServiceProtos.Content.newBuilder()
|
||||
.setDataExtractionNotification(dataExtractionNotification.build())
|
||||
.applyExpiryMode()
|
||||
.build()
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Couldn't construct data extraction notification proto from: $this")
|
||||
return null
|
||||
|
@@ -1,77 +1,52 @@
|
||||
package org.session.libsession.messaging.messages.control
|
||||
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.session.libsession.messaging.messages.copyExpiration
|
||||
import org.session.libsignal.protos.SignalServiceProtos
|
||||
import org.session.libsignal.protos.SignalServiceProtos.DataMessage.Flags.EXPIRATION_TIMER_UPDATE_VALUE
|
||||
import org.session.libsignal.utilities.Log
|
||||
|
||||
class ExpirationTimerUpdate() : ControlMessage() {
|
||||
/** In the case of a sync message, the public key of the person the message was targeted at.
|
||||
*
|
||||
* **Note:** `nil` if this isn't a sync message.
|
||||
*/
|
||||
var syncTarget: String? = null
|
||||
var duration: Int? = 0
|
||||
|
||||
/** In the case of a sync message, the public key of the person the message was targeted at.
|
||||
*
|
||||
* **Note:** `nil` if this isn't a sync message.
|
||||
*/
|
||||
data class ExpirationTimerUpdate(var syncTarget: String? = null, val isGroup: Boolean = false) : ControlMessage() {
|
||||
override val isSelfSendValid: Boolean = true
|
||||
|
||||
override fun isValid(): Boolean {
|
||||
if (!super.isValid()) return false
|
||||
return duration != null
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "ExpirationTimerUpdate"
|
||||
private val storage = MessagingModuleConfiguration.shared.storage
|
||||
|
||||
fun fromProto(proto: SignalServiceProtos.Content): ExpirationTimerUpdate? {
|
||||
val dataMessageProto = if (proto.hasDataMessage()) proto.dataMessage else return null
|
||||
val isExpirationTimerUpdate = dataMessageProto.flags.and(SignalServiceProtos.DataMessage.Flags.EXPIRATION_TIMER_UPDATE_VALUE) != 0
|
||||
if (!isExpirationTimerUpdate) return null
|
||||
val syncTarget = dataMessageProto.syncTarget
|
||||
val duration = dataMessageProto.expireTimer
|
||||
return ExpirationTimerUpdate(syncTarget, duration)
|
||||
}
|
||||
}
|
||||
|
||||
constructor(duration: Int) : this() {
|
||||
this.syncTarget = null
|
||||
this.duration = duration
|
||||
}
|
||||
|
||||
internal constructor(syncTarget: String, duration: Int) : this() {
|
||||
this.syncTarget = syncTarget
|
||||
this.duration = duration
|
||||
fun fromProto(proto: SignalServiceProtos.Content): ExpirationTimerUpdate? =
|
||||
proto.dataMessage?.takeIf { it.flags and EXPIRATION_TIMER_UPDATE_VALUE != 0 }?.run {
|
||||
ExpirationTimerUpdate(takeIf { hasSyncTarget() }?.syncTarget, hasGroup()).copyExpiration(proto)
|
||||
}
|
||||
}
|
||||
|
||||
override fun toProto(): SignalServiceProtos.Content? {
|
||||
val duration = duration
|
||||
if (duration == null) {
|
||||
Log.w(TAG, "Couldn't construct expiration timer update proto from: $this")
|
||||
return null
|
||||
val dataMessageProto = SignalServiceProtos.DataMessage.newBuilder().apply {
|
||||
flags = EXPIRATION_TIMER_UPDATE_VALUE
|
||||
expireTimer = expiryMode.expirySeconds.toInt()
|
||||
}
|
||||
val dataMessageProto = SignalServiceProtos.DataMessage.newBuilder()
|
||||
dataMessageProto.flags = SignalServiceProtos.DataMessage.Flags.EXPIRATION_TIMER_UPDATE_VALUE
|
||||
dataMessageProto.expireTimer = duration
|
||||
// Sync target
|
||||
if (syncTarget != null) {
|
||||
dataMessageProto.syncTarget = syncTarget
|
||||
}
|
||||
syncTarget?.let { dataMessageProto.syncTarget = it }
|
||||
// Group context
|
||||
if (MessagingModuleConfiguration.shared.storage.isClosedGroup(recipient!!)) {
|
||||
if (storage.isClosedGroup(recipient!!)) {
|
||||
try {
|
||||
setGroupContext(dataMessageProto)
|
||||
dataMessageProto.setGroupContext()
|
||||
} catch(e: Exception) {
|
||||
Log.w(VisibleMessage.TAG, "Couldn't construct visible message proto from: $this")
|
||||
Log.w(TAG, "Couldn't construct visible message proto from: $this", e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
val contentProto = SignalServiceProtos.Content.newBuilder()
|
||||
try {
|
||||
contentProto.dataMessage = dataMessageProto.build()
|
||||
return contentProto.build()
|
||||
return try {
|
||||
SignalServiceProtos.Content.newBuilder()
|
||||
.setDataMessage(dataMessageProto)
|
||||
.applyExpiryMode()
|
||||
.build()
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Couldn't construct expiration timer update proto from: $this")
|
||||
return null
|
||||
Log.w(TAG, "Couldn't construct expiration timer update proto from: $this", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,17 +1,26 @@
|
||||
package org.session.libsession.messaging.messages.control
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import org.session.libsession.messaging.messages.copyExpiration
|
||||
import org.session.libsession.messaging.messages.visible.Profile
|
||||
import org.session.libsignal.protos.SignalServiceProtos
|
||||
import org.session.libsignal.utilities.Log
|
||||
|
||||
class MessageRequestResponse(val isApproved: Boolean) : ControlMessage() {
|
||||
class MessageRequestResponse(val isApproved: Boolean, var profile: Profile? = null) : ControlMessage() {
|
||||
|
||||
override val isSelfSendValid: Boolean = true
|
||||
|
||||
override fun toProto(): SignalServiceProtos.Content? {
|
||||
val profileProto = SignalServiceProtos.DataMessage.LokiProfile.newBuilder()
|
||||
profile?.displayName?.let { profileProto.displayName = it }
|
||||
profile?.profilePictureURL?.let { profileProto.profilePicture = it }
|
||||
val messageRequestResponseProto = SignalServiceProtos.MessageRequestResponse.newBuilder()
|
||||
.setIsApproved(isApproved)
|
||||
.setProfile(profileProto.build())
|
||||
profile?.profileKey?.let { messageRequestResponseProto.profileKey = ByteString.copyFrom(it) }
|
||||
return try {
|
||||
SignalServiceProtos.Content.newBuilder()
|
||||
.applyExpiryMode()
|
||||
.setMessageRequestResponse(messageRequestResponseProto.build())
|
||||
.build()
|
||||
} catch (e: Exception) {
|
||||
@@ -26,8 +35,14 @@ class MessageRequestResponse(val isApproved: Boolean) : ControlMessage() {
|
||||
fun fromProto(proto: SignalServiceProtos.Content): MessageRequestResponse? {
|
||||
val messageRequestResponseProto = if (proto.hasMessageRequestResponse()) proto.messageRequestResponse else return null
|
||||
val isApproved = messageRequestResponseProto.isApproved
|
||||
return MessageRequestResponse(isApproved)
|
||||
val profileProto = messageRequestResponseProto.profile
|
||||
val profile = Profile().apply {
|
||||
displayName = profileProto.displayName
|
||||
profileKey = if (messageRequestResponseProto.hasProfileKey()) messageRequestResponseProto.profileKey.toByteArray() else null
|
||||
profilePictureURL = profileProto.profilePicture
|
||||
}
|
||||
return MessageRequestResponse(isApproved, profile)
|
||||
.copyExpiration(proto)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
package org.session.libsession.messaging.messages.control
|
||||
|
||||
import org.session.libsession.messaging.messages.copyExpiration
|
||||
import org.session.libsignal.protos.SignalServiceProtos
|
||||
import org.session.libsignal.utilities.Log
|
||||
|
||||
@@ -22,10 +23,11 @@ class ReadReceipt() : ControlMessage() {
|
||||
val timestamps = receiptProto.timestampList
|
||||
if (timestamps.isEmpty()) return null
|
||||
return ReadReceipt(timestamps = timestamps)
|
||||
.copyExpiration(proto)
|
||||
}
|
||||
}
|
||||
|
||||
internal constructor(timestamps: List<Long>?) : this() {
|
||||
constructor(timestamps: List<Long>?) : this() {
|
||||
this.timestamps = timestamps
|
||||
}
|
||||
|
||||
@@ -35,16 +37,18 @@ class ReadReceipt() : ControlMessage() {
|
||||
Log.w(TAG, "Couldn't construct read receipt proto from: $this")
|
||||
return null
|
||||
}
|
||||
val receiptProto = SignalServiceProtos.ReceiptMessage.newBuilder()
|
||||
receiptProto.type = SignalServiceProtos.ReceiptMessage.Type.READ
|
||||
receiptProto.addAllTimestamp(timestamps.asIterable())
|
||||
val contentProto = SignalServiceProtos.Content.newBuilder()
|
||||
try {
|
||||
contentProto.receiptMessage = receiptProto.build()
|
||||
return contentProto.build()
|
||||
|
||||
return try {
|
||||
SignalServiceProtos.Content.newBuilder()
|
||||
.setReceiptMessage(
|
||||
SignalServiceProtos.ReceiptMessage.newBuilder()
|
||||
.setType(SignalServiceProtos.ReceiptMessage.Type.READ)
|
||||
.addAllTimestamp(timestamps.asIterable()).build()
|
||||
).applyExpiryMode()
|
||||
.build()
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Couldn't construct read receipt proto from: $this")
|
||||
return null
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,34 @@
|
||||
package org.session.libsession.messaging.messages.control
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import org.session.libsignal.protos.SignalServiceProtos
|
||||
import org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage
|
||||
|
||||
class SharedConfigurationMessage(val kind: SharedConfigMessage.Kind, val data: ByteArray, val seqNo: Long): ControlMessage() {
|
||||
|
||||
override val ttl: Long = 30 * 24 * 60 * 60 * 1000L
|
||||
override val isSelfSendValid: Boolean = true
|
||||
|
||||
companion object {
|
||||
fun fromProto(proto: SignalServiceProtos.Content): SharedConfigurationMessage? =
|
||||
proto.takeIf { it.hasSharedConfigMessage() }?.sharedConfigMessage
|
||||
?.takeIf { it.hasKind() && it.hasData() }
|
||||
?.run { SharedConfigurationMessage(kind, data.toByteArray(), seqno) }
|
||||
}
|
||||
|
||||
override fun isValid(): Boolean {
|
||||
if (!super.isValid()) return false
|
||||
return data.isNotEmpty() && seqNo >= 0
|
||||
}
|
||||
|
||||
override fun toProto(): SignalServiceProtos.Content? {
|
||||
val sharedConfigurationMessage = SharedConfigMessage.newBuilder()
|
||||
.setKind(kind)
|
||||
.setSeqno(seqNo)
|
||||
.setData(ByteString.copyFrom(data))
|
||||
.build()
|
||||
return SignalServiceProtos.Content.newBuilder()
|
||||
.setSharedConfigMessage(sharedConfigurationMessage)
|
||||
.build()
|
||||
}
|
||||
}
|
@@ -1,12 +1,13 @@
|
||||
package org.session.libsession.messaging.messages.control
|
||||
|
||||
import org.session.libsession.messaging.messages.copyExpiration
|
||||
import org.session.libsignal.protos.SignalServiceProtos
|
||||
import org.session.libsignal.utilities.Log
|
||||
|
||||
class TypingIndicator() : ControlMessage() {
|
||||
var kind: Kind? = null
|
||||
|
||||
override val ttl: Long = 20 * 1000
|
||||
override val defaultTtl: Long = 20 * 1000
|
||||
|
||||
override fun isValid(): Boolean {
|
||||
if (!super.isValid()) return false
|
||||
@@ -20,6 +21,7 @@ class TypingIndicator() : ControlMessage() {
|
||||
val typingIndicatorProto = if (proto.hasTypingMessage()) proto.typingMessage else return null
|
||||
val kind = Kind.fromProto(typingIndicatorProto.action)
|
||||
return TypingIndicator(kind = kind)
|
||||
.copyExpiration(proto)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,16 +56,14 @@ class TypingIndicator() : ControlMessage() {
|
||||
Log.w(TAG, "Couldn't construct typing indicator proto from: $this")
|
||||
return null
|
||||
}
|
||||
val typingIndicatorProto = SignalServiceProtos.TypingMessage.newBuilder()
|
||||
typingIndicatorProto.timestamp = timestamp
|
||||
typingIndicatorProto.action = kind.toProto()
|
||||
val contentProto = SignalServiceProtos.Content.newBuilder()
|
||||
try {
|
||||
contentProto.typingMessage = typingIndicatorProto.build()
|
||||
return contentProto.build()
|
||||
return try {
|
||||
SignalServiceProtos.Content.newBuilder()
|
||||
.setTypingMessage(SignalServiceProtos.TypingMessage.newBuilder().setTimestamp(timestamp).setAction(kind.toProto()).build())
|
||||
.applyExpiryMode()
|
||||
.build()
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Couldn't construct typing indicator proto from: $this")
|
||||
return null
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,11 +1,10 @@
|
||||
package org.session.libsession.messaging.messages.control
|
||||
|
||||
import org.session.libsession.messaging.messages.copyExpiration
|
||||
import org.session.libsignal.protos.SignalServiceProtos
|
||||
import org.session.libsignal.utilities.Log
|
||||
|
||||
class UnsendRequest(): ControlMessage() {
|
||||
var timestamp: Long? = null
|
||||
var author: String? = null
|
||||
class UnsendRequest(var timestamp: Long? = null, var author: String? = null): ControlMessage() {
|
||||
|
||||
override val isSelfSendValid: Boolean = true
|
||||
|
||||
@@ -19,17 +18,8 @@ class UnsendRequest(): ControlMessage() {
|
||||
companion object {
|
||||
const val TAG = "UnsendRequest"
|
||||
|
||||
fun fromProto(proto: SignalServiceProtos.Content): UnsendRequest? {
|
||||
val unsendRequestProto = if (proto.hasUnsendRequest()) proto.unsendRequest else return null
|
||||
val timestamp = unsendRequestProto.timestamp
|
||||
val author = unsendRequestProto.author
|
||||
return UnsendRequest(timestamp, author)
|
||||
}
|
||||
}
|
||||
|
||||
constructor(timestamp: Long, author: String) : this() {
|
||||
this.timestamp = timestamp
|
||||
this.author = author
|
||||
fun fromProto(proto: SignalServiceProtos.Content): UnsendRequest? =
|
||||
proto.takeIf { it.hasUnsendRequest() }?.unsendRequest?.run { UnsendRequest(timestamp, author) }?.copyExpiration(proto)
|
||||
}
|
||||
|
||||
override fun toProto(): SignalServiceProtos.Content? {
|
||||
@@ -39,16 +29,14 @@ class UnsendRequest(): ControlMessage() {
|
||||
Log.w(TAG, "Couldn't construct unsend request proto from: $this")
|
||||
return null
|
||||
}
|
||||
val unsendRequestProto = SignalServiceProtos.UnsendRequest.newBuilder()
|
||||
unsendRequestProto.timestamp = timestamp
|
||||
unsendRequestProto.author = author
|
||||
val contentProto = SignalServiceProtos.Content.newBuilder()
|
||||
try {
|
||||
contentProto.unsendRequest = unsendRequestProto.build()
|
||||
return contentProto.build()
|
||||
return try {
|
||||
SignalServiceProtos.Content.newBuilder()
|
||||
.setUnsendRequest(SignalServiceProtos.UnsendRequest.newBuilder().setTimestamp(timestamp).setAuthor(author).build())
|
||||
.applyExpiryMode()
|
||||
.build()
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Couldn't construct unsend request proto from: $this")
|
||||
return null
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -26,9 +26,11 @@ public class IncomingMediaMessage {
|
||||
private final long sentTimeMillis;
|
||||
private final int subscriptionId;
|
||||
private final long expiresIn;
|
||||
private final long expireStartedAt;
|
||||
private final boolean expirationUpdate;
|
||||
private final boolean unidentified;
|
||||
private final boolean messageRequestResponse;
|
||||
private final boolean hasMention;
|
||||
|
||||
private final DataExtractionNotificationInfoMessage dataExtractionNotification;
|
||||
private final QuoteModel quote;
|
||||
@@ -41,9 +43,11 @@ public class IncomingMediaMessage {
|
||||
long sentTimeMillis,
|
||||
int subscriptionId,
|
||||
long expiresIn,
|
||||
long expireStartedAt,
|
||||
boolean expirationUpdate,
|
||||
boolean unidentified,
|
||||
boolean messageRequestResponse,
|
||||
boolean hasMention,
|
||||
Optional<String> body,
|
||||
Optional<SignalServiceGroup> group,
|
||||
Optional<List<SignalServiceAttachment>> attachments,
|
||||
@@ -58,11 +62,13 @@ public class IncomingMediaMessage {
|
||||
this.body = body.orNull();
|
||||
this.subscriptionId = subscriptionId;
|
||||
this.expiresIn = expiresIn;
|
||||
this.expireStartedAt = expireStartedAt;
|
||||
this.expirationUpdate = expirationUpdate;
|
||||
this.dataExtractionNotification = dataExtractionNotification.orNull();
|
||||
this.quote = quote.orNull();
|
||||
this.unidentified = unidentified;
|
||||
this.messageRequestResponse = messageRequestResponse;
|
||||
this.hasMention = hasMention;
|
||||
|
||||
if (group.isPresent()) this.groupId = Address.fromSerialized(GroupUtil.INSTANCE.getEncodedId(group.get()));
|
||||
else this.groupId = null;
|
||||
@@ -75,13 +81,15 @@ public class IncomingMediaMessage {
|
||||
public static IncomingMediaMessage from(VisibleMessage message,
|
||||
Address from,
|
||||
long expiresIn,
|
||||
long expireStartedAt,
|
||||
Optional<SignalServiceGroup> group,
|
||||
List<SignalServiceAttachment> attachments,
|
||||
Optional<QuoteModel> quote,
|
||||
Optional<List<LinkPreview>> linkPreviews)
|
||||
{
|
||||
return new IncomingMediaMessage(from, message.getSentTimestamp(), -1, expiresIn, false,
|
||||
false, false, Optional.fromNullable(message.getText()), group, Optional.fromNullable(attachments), quote, Optional.absent(), linkPreviews, Optional.absent());
|
||||
return new IncomingMediaMessage(from, message.getSentTimestamp(), -1, expiresIn, expireStartedAt, false,
|
||||
false, false, message.getHasMention(), Optional.fromNullable(message.getText()),
|
||||
group, Optional.fromNullable(attachments), quote, Optional.absent(), linkPreviews, Optional.absent());
|
||||
}
|
||||
|
||||
public int getSubscriptionId() {
|
||||
@@ -120,10 +128,18 @@ public class IncomingMediaMessage {
|
||||
return expiresIn;
|
||||
}
|
||||
|
||||
public long getExpireStartedAt() {
|
||||
return expireStartedAt;
|
||||
}
|
||||
|
||||
public boolean isGroupMessage() {
|
||||
return groupId != null;
|
||||
}
|
||||
|
||||
public boolean hasMention() {
|
||||
return hasMention;
|
||||
}
|
||||
|
||||
public boolean isScreenshotDataExtraction() {
|
||||
if (dataExtractionNotification == null) return false;
|
||||
else {
|
||||
|
@@ -41,26 +41,28 @@ public class IncomingTextMessage implements Parcelable {
|
||||
private final boolean push;
|
||||
private final int subscriptionId;
|
||||
private final long expiresInMillis;
|
||||
private final long expireStartedAt;
|
||||
private final boolean unidentified;
|
||||
private final int callType;
|
||||
private final boolean hasMention;
|
||||
|
||||
private boolean isOpenGroupInvitation = false;
|
||||
|
||||
public IncomingTextMessage(Address sender, int senderDeviceId, long sentTimestampMillis,
|
||||
String encodedBody, Optional<SignalServiceGroup> group,
|
||||
long expiresInMillis, boolean unidentified) {
|
||||
this(sender, senderDeviceId, sentTimestampMillis, encodedBody, group, expiresInMillis, unidentified, -1);
|
||||
long expiresInMillis, long expireStartedAt, boolean unidentified, boolean hasMention) {
|
||||
this(sender, senderDeviceId, sentTimestampMillis, encodedBody, group, expiresInMillis, expireStartedAt, unidentified, -1, hasMention);
|
||||
}
|
||||
|
||||
public IncomingTextMessage(Address sender, int senderDeviceId, long sentTimestampMillis,
|
||||
String encodedBody, Optional<SignalServiceGroup> group,
|
||||
long expiresInMillis, boolean unidentified, int callType) {
|
||||
this(sender, senderDeviceId, sentTimestampMillis, encodedBody, group, expiresInMillis, unidentified, callType, true);
|
||||
long expiresInMillis, long expireStartedAt, boolean unidentified, int callType, boolean hasMention) {
|
||||
this(sender, senderDeviceId, sentTimestampMillis, encodedBody, group, expiresInMillis, expireStartedAt, unidentified, callType, hasMention, true);
|
||||
}
|
||||
|
||||
public IncomingTextMessage(Address sender, int senderDeviceId, long sentTimestampMillis,
|
||||
String encodedBody, Optional<SignalServiceGroup> group,
|
||||
long expiresInMillis, boolean unidentified, int callType, boolean isPush) {
|
||||
long expiresInMillis, long expireStartedAt, boolean unidentified, int callType, boolean hasMention, boolean isPush) {
|
||||
this.message = encodedBody;
|
||||
this.sender = sender;
|
||||
this.senderDeviceId = senderDeviceId;
|
||||
@@ -72,8 +74,10 @@ public class IncomingTextMessage implements Parcelable {
|
||||
this.push = isPush;
|
||||
this.subscriptionId = -1;
|
||||
this.expiresInMillis = expiresInMillis;
|
||||
this.expireStartedAt = expireStartedAt;
|
||||
this.unidentified = unidentified;
|
||||
this.callType = callType;
|
||||
this.hasMention = hasMention;
|
||||
|
||||
if (group.isPresent()) {
|
||||
this.groupId = Address.fromSerialized(GroupUtil.getEncodedId(group.get()));
|
||||
@@ -95,9 +99,11 @@ public class IncomingTextMessage implements Parcelable {
|
||||
this.push = (in.readInt() == 1);
|
||||
this.subscriptionId = in.readInt();
|
||||
this.expiresInMillis = in.readLong();
|
||||
this.expireStartedAt = in.readLong();
|
||||
this.unidentified = in.readInt() == 1;
|
||||
this.isOpenGroupInvitation = in.readInt() == 1;
|
||||
this.callType = in.readInt();
|
||||
this.hasMention = in.readInt() == 1;
|
||||
}
|
||||
|
||||
public IncomingTextMessage(IncomingTextMessage base, String newBody) {
|
||||
@@ -113,27 +119,33 @@ public class IncomingTextMessage implements Parcelable {
|
||||
this.push = base.isPush();
|
||||
this.subscriptionId = base.getSubscriptionId();
|
||||
this.expiresInMillis = base.getExpiresIn();
|
||||
this.expireStartedAt = base.getExpireStartedAt();
|
||||
this.unidentified = base.isUnidentified();
|
||||
this.isOpenGroupInvitation = base.isOpenGroupInvitation();
|
||||
this.callType = base.callType;
|
||||
this.hasMention = base.hasMention;
|
||||
}
|
||||
|
||||
public static IncomingTextMessage from(VisibleMessage message,
|
||||
Address sender,
|
||||
Optional<SignalServiceGroup> group,
|
||||
long expiresInMillis)
|
||||
long expiresInMillis,
|
||||
long expireStartedAt)
|
||||
{
|
||||
return new IncomingTextMessage(sender, 1, message.getSentTimestamp(), message.getText(), group, expiresInMillis, false);
|
||||
return new IncomingTextMessage(sender, 1, message.getSentTimestamp(), message.getText(), group, expiresInMillis, expireStartedAt, false, message.getHasMention());
|
||||
}
|
||||
|
||||
public static IncomingTextMessage fromOpenGroupInvitation(OpenGroupInvitation openGroupInvitation, Address sender, Long sentTimestamp)
|
||||
{
|
||||
public static IncomingTextMessage fromOpenGroupInvitation(OpenGroupInvitation openGroupInvitation,
|
||||
Address sender,
|
||||
Long sentTimestamp,
|
||||
long expiresInMillis,
|
||||
long expireStartedAt) {
|
||||
String url = openGroupInvitation.getUrl();
|
||||
String name = openGroupInvitation.getName();
|
||||
if (url == null || name == null) { return null; }
|
||||
// FIXME: Doing toJSON() to get the body here is weird
|
||||
String body = UpdateMessageData.Companion.buildOpenGroupInvitation(url, name).toJSON();
|
||||
IncomingTextMessage incomingTextMessage = new IncomingTextMessage(sender, 1, sentTimestamp, body, Optional.absent(), 0, false);
|
||||
IncomingTextMessage incomingTextMessage = new IncomingTextMessage(sender, 1, sentTimestamp, body, Optional.absent(), expiresInMillis, expireStartedAt, false, false);
|
||||
incomingTextMessage.isOpenGroupInvitation = true;
|
||||
return incomingTextMessage;
|
||||
}
|
||||
@@ -141,8 +153,10 @@ public class IncomingTextMessage implements Parcelable {
|
||||
public static IncomingTextMessage fromCallInfo(CallMessageType callMessageType,
|
||||
Address sender,
|
||||
Optional<SignalServiceGroup> group,
|
||||
long sentTimestamp) {
|
||||
return new IncomingTextMessage(sender, 1, sentTimestamp, null, group, 0, false, callMessageType.ordinal(), false);
|
||||
long sentTimestamp,
|
||||
long expiresInMillis,
|
||||
long expireStartedAt) {
|
||||
return new IncomingTextMessage(sender, 1, sentTimestamp, null, group, expiresInMillis, expireStartedAt, false, callMessageType.ordinal(), false, false);
|
||||
}
|
||||
|
||||
public int getSubscriptionId() {
|
||||
@@ -153,6 +167,10 @@ public class IncomingTextMessage implements Parcelable {
|
||||
return expiresInMillis;
|
||||
}
|
||||
|
||||
public long getExpireStartedAt() {
|
||||
return expireStartedAt;
|
||||
}
|
||||
|
||||
public long getSentTimestampMillis() {
|
||||
return sentTimestampMillis;
|
||||
}
|
||||
@@ -207,6 +225,8 @@ public class IncomingTextMessage implements Parcelable {
|
||||
|
||||
public boolean isOpenGroupInvitation() { return isOpenGroupInvitation; }
|
||||
|
||||
public boolean hasMention() { return hasMention; }
|
||||
|
||||
public boolean isCallInfo() {
|
||||
int callMessageTypeLength = CallMessageType.values().length;
|
||||
return callType >= 0 && callType < callMessageTypeLength;
|
||||
@@ -240,5 +260,6 @@ public class IncomingTextMessage implements Parcelable {
|
||||
out.writeInt(unidentified ? 1 : 0);
|
||||
out.writeInt(isOpenGroupInvitation ? 1 : 0);
|
||||
out.writeInt(callType);
|
||||
out.writeInt(hasMention ? 1 : 0);
|
||||
}
|
||||
}
|
||||
|
@@ -11,9 +11,9 @@ public class OutgoingExpirationUpdateMessage extends OutgoingSecureMediaMessage
|
||||
|
||||
private final String groupId;
|
||||
|
||||
public OutgoingExpirationUpdateMessage(Recipient recipient, long sentTimeMillis, long expiresIn, String groupId) {
|
||||
public OutgoingExpirationUpdateMessage(Recipient recipient, long sentTimeMillis, long expiresIn, long expireStartedAt, String groupId) {
|
||||
super(recipient, "", new LinkedList<Attachment>(), sentTimeMillis,
|
||||
DistributionTypes.CONVERSATION, expiresIn, null, Collections.emptyList(),
|
||||
DistributionTypes.CONVERSATION, expiresIn, expireStartedAt, null, Collections.emptyList(),
|
||||
Collections.emptyList());
|
||||
this.groupId = groupId;
|
||||
}
|
||||
|
@@ -24,6 +24,7 @@ public class OutgoingGroupMediaMessage extends OutgoingSecureMediaMessage {
|
||||
@Nullable final Attachment avatar,
|
||||
long sentTime,
|
||||
long expireIn,
|
||||
long expireStartedAt,
|
||||
boolean updateMessage,
|
||||
@Nullable QuoteModel quote,
|
||||
@NonNull List<Contact> contacts,
|
||||
@@ -32,7 +33,7 @@ public class OutgoingGroupMediaMessage extends OutgoingSecureMediaMessage {
|
||||
super(recipient, body,
|
||||
new LinkedList<Attachment>() {{if (avatar != null) add(avatar);}},
|
||||
sentTime,
|
||||
DistributionTypes.CONVERSATION, expireIn, quote, contacts, previews);
|
||||
DistributionTypes.CONVERSATION, expireIn, expireStartedAt, quote, contacts, previews);
|
||||
|
||||
this.groupID = groupId;
|
||||
this.isUpdateMessage = updateMessage;
|
||||
|
@@ -4,13 +4,13 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.session.libsession.messaging.messages.visible.VisibleMessage;
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
|
||||
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview;
|
||||
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel;
|
||||
import org.session.libsession.utilities.Contact;
|
||||
import org.session.libsession.utilities.DistributionTypes;
|
||||
import org.session.libsession.utilities.IdentityKeyMismatch;
|
||||
import org.session.libsession.utilities.NetworkFailure;
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
|
||||
import org.session.libsession.utilities.Contact;
|
||||
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview;
|
||||
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel;
|
||||
import org.session.libsession.utilities.recipients.Recipient;
|
||||
|
||||
import java.util.Collections;
|
||||
@@ -26,6 +26,7 @@ public class OutgoingMediaMessage {
|
||||
private final int distributionType;
|
||||
private final int subscriptionId;
|
||||
private final long expiresIn;
|
||||
private final long expireStartedAt;
|
||||
private final QuoteModel outgoingQuote;
|
||||
|
||||
private final List<NetworkFailure> networkFailures = new LinkedList<>();
|
||||
@@ -35,7 +36,7 @@ public class OutgoingMediaMessage {
|
||||
|
||||
public OutgoingMediaMessage(Recipient recipient, String message,
|
||||
List<Attachment> attachments, long sentTimeMillis,
|
||||
int subscriptionId, long expiresIn,
|
||||
int subscriptionId, long expiresIn, long expireStartedAt,
|
||||
int distributionType,
|
||||
@Nullable QuoteModel outgoingQuote,
|
||||
@NonNull List<Contact> contacts,
|
||||
@@ -50,6 +51,7 @@ public class OutgoingMediaMessage {
|
||||
this.attachments = attachments;
|
||||
this.subscriptionId = subscriptionId;
|
||||
this.expiresIn = expiresIn;
|
||||
this.expireStartedAt = expireStartedAt;
|
||||
this.outgoingQuote = outgoingQuote;
|
||||
|
||||
this.contacts.addAll(contacts);
|
||||
@@ -66,6 +68,7 @@ public class OutgoingMediaMessage {
|
||||
this.sentTimeMillis = that.sentTimeMillis;
|
||||
this.subscriptionId = that.subscriptionId;
|
||||
this.expiresIn = that.expiresIn;
|
||||
this.expireStartedAt = that.expireStartedAt;
|
||||
this.outgoingQuote = that.outgoingQuote;
|
||||
|
||||
this.identityKeyMismatches.addAll(that.identityKeyMismatches);
|
||||
@@ -78,15 +81,17 @@ public class OutgoingMediaMessage {
|
||||
Recipient recipient,
|
||||
List<Attachment> attachments,
|
||||
@Nullable QuoteModel outgoingQuote,
|
||||
@Nullable LinkPreview linkPreview)
|
||||
@Nullable LinkPreview linkPreview,
|
||||
long expiresInMillis,
|
||||
long expireStartedAt)
|
||||
{
|
||||
List<LinkPreview> previews = Collections.emptyList();
|
||||
if (linkPreview != null) {
|
||||
previews = Collections.singletonList(linkPreview);
|
||||
}
|
||||
return new OutgoingMediaMessage(recipient, message.getText(), attachments, message.getSentTimestamp(), -1,
|
||||
recipient.getExpireMessages() * 1000, DistributionTypes.DEFAULT, outgoingQuote, Collections.emptyList(),
|
||||
previews, Collections.emptyList(), Collections.emptyList());
|
||||
expiresInMillis, expireStartedAt, DistributionTypes.DEFAULT, outgoingQuote,
|
||||
Collections.emptyList(), previews, Collections.emptyList(), Collections.emptyList());
|
||||
}
|
||||
|
||||
public Recipient getRecipient() {
|
||||
@@ -123,6 +128,10 @@ public class OutgoingMediaMessage {
|
||||
return expiresIn;
|
||||
}
|
||||
|
||||
public long getExpireStartedAt() {
|
||||
return expireStartedAt;
|
||||
}
|
||||
|
||||
public @Nullable QuoteModel getOutgoingQuote() {
|
||||
return outgoingQuote;
|
||||
}
|
||||
|
@@ -19,11 +19,12 @@ public class OutgoingSecureMediaMessage extends OutgoingMediaMessage {
|
||||
long sentTimeMillis,
|
||||
int distributionType,
|
||||
long expiresIn,
|
||||
long expireStartedAt,
|
||||
@Nullable QuoteModel quote,
|
||||
@NonNull List<Contact> contacts,
|
||||
@NonNull List<LinkPreview> previews)
|
||||
{
|
||||
super(recipient, body, attachments, sentTimeMillis, -1, expiresIn, distributionType, quote, contacts, previews, Collections.emptyList(), Collections.emptyList());
|
||||
super(recipient, body, attachments, sentTimeMillis, -1, expiresIn, expireStartedAt, distributionType, quote, contacts, previews, Collections.emptyList(), Collections.emptyList());
|
||||
}
|
||||
|
||||
public OutgoingSecureMediaMessage(OutgoingMediaMessage base) {
|
||||
|
@@ -10,28 +10,30 @@ public class OutgoingTextMessage {
|
||||
private final String message;
|
||||
private final int subscriptionId;
|
||||
private final long expiresIn;
|
||||
private final long expireStartedAt;
|
||||
private final long sentTimestampMillis;
|
||||
private boolean isOpenGroupInvitation = false;
|
||||
|
||||
public OutgoingTextMessage(Recipient recipient, String message, long expiresIn, int subscriptionId, long sentTimestampMillis) {
|
||||
public OutgoingTextMessage(Recipient recipient, String message, long expiresIn, long expireStartedAt, int subscriptionId, long sentTimestampMillis) {
|
||||
this.recipient = recipient;
|
||||
this.message = message;
|
||||
this.expiresIn = expiresIn;
|
||||
this.expireStartedAt= expireStartedAt;
|
||||
this.subscriptionId = subscriptionId;
|
||||
this.sentTimestampMillis = sentTimestampMillis;
|
||||
}
|
||||
|
||||
public static OutgoingTextMessage from(VisibleMessage message, Recipient recipient) {
|
||||
return new OutgoingTextMessage(recipient, message.getText(), recipient.getExpireMessages() * 1000, -1, message.getSentTimestamp());
|
||||
public static OutgoingTextMessage from(VisibleMessage message, Recipient recipient, long expiresInMillis, long expireStartedAt) {
|
||||
return new OutgoingTextMessage(recipient, message.getText(), expiresInMillis, expireStartedAt, -1, message.getSentTimestamp());
|
||||
}
|
||||
|
||||
public static OutgoingTextMessage fromOpenGroupInvitation(OpenGroupInvitation openGroupInvitation, Recipient recipient, Long sentTimestamp) {
|
||||
public static OutgoingTextMessage fromOpenGroupInvitation(OpenGroupInvitation openGroupInvitation, Recipient recipient, Long sentTimestamp, long expiresInMillis, long expireStartedAt) {
|
||||
String url = openGroupInvitation.getUrl();
|
||||
String name = openGroupInvitation.getName();
|
||||
if (url == null || name == null) { return null; }
|
||||
// FIXME: Doing toJSON() to get the body here is weird
|
||||
String body = UpdateMessageData.Companion.buildOpenGroupInvitation(url, name).toJSON();
|
||||
OutgoingTextMessage outgoingTextMessage = new OutgoingTextMessage(recipient, body, 0, -1, sentTimestamp);
|
||||
OutgoingTextMessage outgoingTextMessage = new OutgoingTextMessage(recipient, body, expiresInMillis, expireStartedAt, -1, sentTimestamp);
|
||||
outgoingTextMessage.isOpenGroupInvitation = true;
|
||||
return outgoingTextMessage;
|
||||
}
|
||||
@@ -40,6 +42,10 @@ public class OutgoingTextMessage {
|
||||
return expiresIn;
|
||||
}
|
||||
|
||||
public long getExpireStartedAt() {
|
||||
return expireStartedAt;
|
||||
}
|
||||
|
||||
public int getSubscriptionId() {
|
||||
return subscriptionId;
|
||||
}
|
||||
|
@@ -4,10 +4,11 @@ import com.google.protobuf.ByteString
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.session.libsignal.protos.SignalServiceProtos
|
||||
|
||||
class Profile() {
|
||||
var displayName: String? = null
|
||||
var profileKey: ByteArray? = null
|
||||
class Profile(
|
||||
var displayName: String? = null,
|
||||
var profileKey: ByteArray? = null,
|
||||
var profilePictureURL: String? = null
|
||||
) {
|
||||
|
||||
companion object {
|
||||
const val TAG = "Profile"
|
||||
@@ -25,12 +26,6 @@ class Profile() {
|
||||
}
|
||||
}
|
||||
|
||||
internal constructor(displayName: String, profileKey: ByteArray? = null, profilePictureURL: String? = null) : this() {
|
||||
this.displayName = displayName
|
||||
this.profileKey = profileKey
|
||||
this.profilePictureURL = profilePictureURL
|
||||
}
|
||||
|
||||
fun toProto(): SignalServiceProtos.DataMessage? {
|
||||
val displayName = displayName
|
||||
if (displayName == null) {
|
||||
|
@@ -3,27 +3,29 @@ package org.session.libsession.messaging.messages.visible
|
||||
import com.goterl.lazysodium.BuildConfig
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.messages.Message
|
||||
import org.session.libsession.messaging.messages.copyExpiration
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.session.libsignal.protos.SignalServiceProtos
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment as SignalAttachment
|
||||
|
||||
class VisibleMessage : Message() {
|
||||
/** In the case of a sync message, the public key of the person the message was targeted at.
|
||||
*
|
||||
* **Note:** `nil` if this isn't a sync message.
|
||||
*/
|
||||
var syncTarget: String? = null
|
||||
var text: String? = null
|
||||
val attachmentIDs: MutableList<Long> = mutableListOf()
|
||||
var quote: Quote? = null
|
||||
var linkPreview: LinkPreview? = null
|
||||
var profile: Profile? = null
|
||||
var openGroupInvitation: OpenGroupInvitation? = null
|
||||
var reaction: Reaction? = null
|
||||
/**
|
||||
* @param syncTarget In the case of a sync message, the public key of the person the message was targeted at.
|
||||
*
|
||||
* **Note:** `nil` if this isn't a sync message.
|
||||
*/
|
||||
data class VisibleMessage(
|
||||
var syncTarget: String? = null,
|
||||
var text: String? = null,
|
||||
val attachmentIDs: MutableList<Long> = mutableListOf(),
|
||||
var quote: Quote? = null,
|
||||
var linkPreview: LinkPreview? = null,
|
||||
var profile: Profile? = null,
|
||||
var openGroupInvitation: OpenGroupInvitation? = null,
|
||||
var reaction: Reaction? = null,
|
||||
var hasMention: Boolean = false,
|
||||
var blocksMessageRequests: Boolean = false
|
||||
) : Message() {
|
||||
|
||||
override val isSelfSendValid: Boolean = true
|
||||
|
||||
@@ -42,49 +44,26 @@ class VisibleMessage : Message() {
|
||||
companion object {
|
||||
const val TAG = "VisibleMessage"
|
||||
|
||||
fun fromProto(proto: SignalServiceProtos.Content): VisibleMessage? {
|
||||
val dataMessage = proto.dataMessage ?: return null
|
||||
val result = VisibleMessage()
|
||||
if (dataMessage.hasSyncTarget()) { result.syncTarget = dataMessage.syncTarget }
|
||||
result.text = dataMessage.body
|
||||
// Attachments are handled in MessageReceiver
|
||||
val quoteProto = if (dataMessage.hasQuote()) dataMessage.quote else null
|
||||
if (quoteProto != null) {
|
||||
val quote = Quote.fromProto(quoteProto)
|
||||
result.quote = quote
|
||||
}
|
||||
val linkPreviewProto = dataMessage.previewList.firstOrNull()
|
||||
if (linkPreviewProto != null) {
|
||||
val linkPreview = LinkPreview.fromProto(linkPreviewProto)
|
||||
result.linkPreview = linkPreview
|
||||
}
|
||||
val openGroupInvitationProto = if (dataMessage.hasOpenGroupInvitation()) dataMessage.openGroupInvitation else null
|
||||
if (openGroupInvitationProto != null) {
|
||||
val openGroupInvitation = OpenGroupInvitation.fromProto(openGroupInvitationProto)
|
||||
result.openGroupInvitation = openGroupInvitation
|
||||
}
|
||||
// TODO Contact
|
||||
val profile = Profile.fromProto(dataMessage)
|
||||
if (profile != null) { result.profile = profile }
|
||||
val reactionProto = if (dataMessage.hasReaction()) dataMessage.reaction else null
|
||||
if (reactionProto != null) {
|
||||
val reaction = Reaction.fromProto(reactionProto)
|
||||
result.reaction = reaction
|
||||
}
|
||||
return result
|
||||
fun fromProto(proto: SignalServiceProtos.Content): VisibleMessage? =
|
||||
proto.dataMessage?.let { VisibleMessage().apply {
|
||||
if (it.hasSyncTarget()) syncTarget = it.syncTarget
|
||||
text = it.body
|
||||
// Attachments are handled in MessageReceiver
|
||||
if (it.hasQuote()) quote = Quote.fromProto(it.quote)
|
||||
linkPreview = it.previewList.firstOrNull()?.let(LinkPreview::fromProto)
|
||||
if (it.hasOpenGroupInvitation()) openGroupInvitation = it.openGroupInvitation?.let(OpenGroupInvitation::fromProto)
|
||||
// TODO Contact
|
||||
profile = Profile.fromProto(it)
|
||||
if (it.hasReaction()) reaction = it.reaction?.let(Reaction::fromProto)
|
||||
blocksMessageRequests = it.hasBlocksCommunityMessageRequests() && it.blocksCommunityMessageRequests
|
||||
}.copyExpiration(proto)
|
||||
}
|
||||
}
|
||||
|
||||
override fun toProto(): SignalServiceProtos.Content? {
|
||||
val proto = SignalServiceProtos.Content.newBuilder()
|
||||
val dataMessage: SignalServiceProtos.DataMessage.Builder
|
||||
// Profile
|
||||
val profileProto = profile?.toProto()
|
||||
dataMessage = if (profileProto != null) {
|
||||
profileProto.toBuilder()
|
||||
} else {
|
||||
SignalServiceProtos.DataMessage.newBuilder()
|
||||
}
|
||||
val dataMessage = profile?.toProto()?.toBuilder() ?: SignalServiceProtos.DataMessage.newBuilder()
|
||||
// Text
|
||||
if (text != null) { dataMessage.body = text }
|
||||
// Quote
|
||||
@@ -119,25 +98,19 @@ class VisibleMessage : Message() {
|
||||
dataMessage.addAllAttachments(pointers)
|
||||
// TODO: Contact
|
||||
// Expiration timer
|
||||
// TODO: We * want * expiration timer updates to be explicit. But currently Android will disable the expiration timer for a conversation
|
||||
// if it receives a message without the current expiration timer value attached to it...
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val context = MessagingModuleConfiguration.shared.context
|
||||
val expiration = if (storage.isClosedGroup(recipient!!)) {
|
||||
Recipient.from(context, Address.fromSerialized(GroupUtil.doubleEncodeGroupID(recipient!!)), false).expireMessages
|
||||
} else {
|
||||
Recipient.from(context, Address.fromSerialized(recipient!!), false).expireMessages
|
||||
}
|
||||
dataMessage.expireTimer = expiration
|
||||
proto.applyExpiryMode()
|
||||
// Group context
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
if (storage.isClosedGroup(recipient!!)) {
|
||||
try {
|
||||
setGroupContext(dataMessage)
|
||||
dataMessage.setGroupContext()
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Couldn't construct visible message proto from: $this")
|
||||
return null
|
||||
}
|
||||
}
|
||||
// Community blocked message requests flag
|
||||
dataMessage.blocksCommunityMessageRequests = blocksMessageRequests
|
||||
// Sync target
|
||||
if (syncTarget != null) {
|
||||
dataMessage.syncTarget = syncTarget
|
||||
@@ -164,4 +137,4 @@ class VisibleMessage : Message() {
|
||||
fun isMediaMessage(): Boolean {
|
||||
return attachmentIDs.isNotEmpty() || quote != null || linkPreview != null || reaction != null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -11,16 +11,19 @@ data class OpenGroup(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val publicKey: String,
|
||||
val imageId: String?,
|
||||
val infoUpdates: Int,
|
||||
val canWrite: Boolean,
|
||||
) {
|
||||
|
||||
constructor(server: String, room: String, name: String, infoUpdates: Int, publicKey: String) : this(
|
||||
constructor(server: String, room: String, publicKey: String, name: String, imageId: String?, canWrite: Boolean, infoUpdates: Int) : this(
|
||||
server = server,
|
||||
room = room,
|
||||
id = "$server.$room",
|
||||
name = name,
|
||||
publicKey = publicKey,
|
||||
imageId = imageId,
|
||||
infoUpdates = infoUpdates,
|
||||
canWrite = canWrite
|
||||
)
|
||||
|
||||
companion object {
|
||||
@@ -29,13 +32,14 @@ data class OpenGroup(
|
||||
return try {
|
||||
val json = JsonUtil.fromJson(jsonAsString)
|
||||
if (!json.has("room")) return null
|
||||
val room = json.get("room").asText().toLowerCase(Locale.US)
|
||||
val server = json.get("server").asText().toLowerCase(Locale.US)
|
||||
val room = json.get("room").asText().lowercase(Locale.US)
|
||||
val server = json.get("server").asText().lowercase(Locale.US)
|
||||
val displayName = json.get("displayName").asText()
|
||||
val publicKey = json.get("publicKey").asText()
|
||||
val imageId = if (json.hasNonNull("imageId")) { json.get("imageId")?.asText() } else { null }
|
||||
val canWrite = json.get("canWrite")?.asText()?.toBoolean() ?: true
|
||||
val infoUpdates = json.get("infoUpdates")?.asText()?.toIntOrNull() ?: 0
|
||||
val capabilities = json.get("capabilities")?.asText()?.split(",") ?: emptyList()
|
||||
OpenGroup(server, room, displayName, infoUpdates, publicKey)
|
||||
OpenGroup(server = server, room = room, name = displayName, publicKey = publicKey, imageId = imageId, canWrite = canWrite, infoUpdates = infoUpdates)
|
||||
} catch (e: Exception) {
|
||||
Log.w("Loki", "Couldn't parse open group from JSON: $jsonAsString.", e);
|
||||
null
|
||||
@@ -53,12 +57,14 @@ data class OpenGroup(
|
||||
}
|
||||
}
|
||||
|
||||
fun toJson(): Map<String,String> = mapOf(
|
||||
fun toJson(): Map<String,String?> = mapOf(
|
||||
"room" to room,
|
||||
"server" to server,
|
||||
"displayName" to name,
|
||||
"publicKey" to publicKey,
|
||||
"displayName" to name,
|
||||
"imageId" to imageId,
|
||||
"infoUpdates" to infoUpdates.toString(),
|
||||
"canWrite" to canWrite.toString()
|
||||
)
|
||||
|
||||
val joinURL: String get() = "$server/$room?public_key=$publicKey"
|
||||
|
@@ -21,8 +21,10 @@ import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPoller.Companion.maxInactivityPeriod
|
||||
import org.session.libsession.messaging.utilities.SessionId
|
||||
import org.session.libsession.messaging.utilities.SodiumUtilities
|
||||
import org.session.libsession.messaging.utilities.SodiumUtilities.sodium
|
||||
import org.session.libsession.snode.OnionRequestAPI
|
||||
import org.session.libsession.snode.OnionResponse
|
||||
import org.session.libsession.snode.SnodeAPI
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.utilities.Base64.decode
|
||||
import org.session.libsignal.utilities.Base64.encodeBytes
|
||||
@@ -47,7 +49,6 @@ object OpenGroupApi {
|
||||
val defaultRooms = MutableSharedFlow<List<DefaultGroup>>(replay = 1)
|
||||
private val hasPerformedInitialPoll = mutableMapOf<String, Boolean>()
|
||||
private var hasUpdatedLastOpenDate = false
|
||||
private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) }
|
||||
private val timeSinceLastOpen by lazy {
|
||||
val context = MessagingModuleConfiguration.shared.context
|
||||
val lastOpenDate = TextSecurePreferences.getLastOpenTimeDate(context)
|
||||
@@ -91,7 +92,7 @@ object OpenGroupApi {
|
||||
val created: Long = 0,
|
||||
val activeUsers: Int = 0,
|
||||
val activeUsersCutoff: Int = 0,
|
||||
val imageId: Long? = null,
|
||||
val imageId: String? = null,
|
||||
val pinnedMessages: List<PinnedMessage> = emptyList(),
|
||||
val admin: Boolean = false,
|
||||
val globalAdmin: Boolean = false,
|
||||
@@ -108,7 +109,24 @@ object OpenGroupApi {
|
||||
val defaultWrite: Boolean = false,
|
||||
val upload: Boolean = false,
|
||||
val defaultUpload: Boolean = false,
|
||||
)
|
||||
) {
|
||||
fun toPollInfo() = RoomPollInfo(
|
||||
token = token,
|
||||
activeUsers = activeUsers,
|
||||
admin = admin,
|
||||
globalAdmin = globalAdmin,
|
||||
moderator = moderator,
|
||||
globalModerator = globalModerator,
|
||||
read = read,
|
||||
defaultRead = defaultRead,
|
||||
defaultAccessible = defaultAccessible,
|
||||
write = write,
|
||||
defaultWrite = defaultWrite,
|
||||
upload = upload,
|
||||
defaultUpload = defaultUpload,
|
||||
details = this
|
||||
)
|
||||
}
|
||||
|
||||
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy::class)
|
||||
data class PinnedMessage(
|
||||
@@ -148,7 +166,7 @@ object OpenGroupApi {
|
||||
)
|
||||
|
||||
enum class Capability {
|
||||
BLIND, REACTIONS
|
||||
SOGS, BLIND, REACTIONS
|
||||
}
|
||||
|
||||
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy::class)
|
||||
@@ -300,8 +318,9 @@ object OpenGroupApi {
|
||||
?: return Promise.ofFail(Error.NoEd25519KeyPair)
|
||||
val urlRequest = urlBuilder.toString()
|
||||
val headers = request.headers.toMutableMap()
|
||||
|
||||
val nonce = sodium.nonce(16)
|
||||
val timestamp = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis())
|
||||
val timestamp = TimeUnit.MILLISECONDS.toSeconds(SnodeAPI.nowWithOffset)
|
||||
var pubKey = ""
|
||||
var signature = ByteArray(Sign.BYTES)
|
||||
var bodyHash = ByteArray(0)
|
||||
@@ -380,7 +399,11 @@ object OpenGroupApi {
|
||||
}
|
||||
return if (request.useOnionRouting) {
|
||||
OnionRequestAPI.sendOnionRequest(requestBuilder.build(), request.server, publicKey).fail { e ->
|
||||
Log.e("SOGS", "Failed onion request", e)
|
||||
when (e) {
|
||||
// No need for the stack trace for HTTP errors
|
||||
is HTTP.HTTPRequestFailedException -> Log.e("SOGS", "Failed onion request: ${e.message}")
|
||||
else -> Log.e("SOGS", "Failed onion request", e)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Promise.ofFail(IllegalStateException("It's currently not allowed to send non onion routed requests."))
|
||||
@@ -392,13 +415,13 @@ object OpenGroupApi {
|
||||
fun downloadOpenGroupProfilePicture(
|
||||
server: String,
|
||||
roomID: String,
|
||||
imageId: Long
|
||||
imageId: String
|
||||
): Promise<ByteArray, Exception> {
|
||||
val request = Request(
|
||||
verb = GET,
|
||||
room = roomID,
|
||||
server = server,
|
||||
endpoint = Endpoint.RoomFileIndividual(roomID, imageId.toString())
|
||||
endpoint = Endpoint.RoomFileIndividual(roomID, imageId)
|
||||
)
|
||||
return getResponseBody(request)
|
||||
}
|
||||
@@ -577,8 +600,7 @@ object OpenGroupApi {
|
||||
// region Message Deletion
|
||||
@JvmStatic
|
||||
fun deleteMessage(serverID: Long, room: String, server: String): Promise<Unit, Exception> {
|
||||
val request =
|
||||
Request(verb = DELETE, room = room, server = server, endpoint = Endpoint.RoomMessageIndividual(room, serverID))
|
||||
val request = Request(verb = DELETE, room = room, server = server, endpoint = Endpoint.RoomMessageIndividual(room, serverID))
|
||||
return send(request).map {
|
||||
Log.d("Loki", "Message deletion successful.")
|
||||
}
|
||||
@@ -634,7 +656,9 @@ object OpenGroupApi {
|
||||
}
|
||||
|
||||
fun banAndDeleteAll(publicKey: String, room: String, server: String): Promise<Unit, Exception> {
|
||||
|
||||
val requests = mutableListOf<BatchRequestInfo<*>>(
|
||||
// Ban request
|
||||
BatchRequestInfo(
|
||||
request = BatchRequest(
|
||||
method = POST,
|
||||
@@ -644,6 +668,7 @@ object OpenGroupApi {
|
||||
endpoint = Endpoint.UserBan(publicKey),
|
||||
responseType = object: TypeReference<Any>(){}
|
||||
),
|
||||
// Delete request
|
||||
BatchRequestInfo(
|
||||
request = BatchRequest(DELETE, "/room/$room/all/$publicKey"),
|
||||
endpoint = Endpoint.RoomDeleteMessages(room, publicKey),
|
||||
@@ -728,7 +753,8 @@ object OpenGroupApi {
|
||||
)
|
||||
}
|
||||
val serverCapabilities = storage.getServerCapabilities(server)
|
||||
if (serverCapabilities.contains(Capability.BLIND.name.lowercase())) {
|
||||
val isAcceptingCommunityRequests = storage.isCheckingCommunityRequests()
|
||||
if (serverCapabilities.contains(Capability.BLIND.name.lowercase()) && isAcceptingCommunityRequests) {
|
||||
requests.add(
|
||||
if (lastInboxMessageId == null) {
|
||||
BatchRequestInfo(
|
||||
@@ -947,5 +973,17 @@ object OpenGroupApi {
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteAllInboxMessages(server: String): Promise<Map<*, *>, java.lang.Exception> {
|
||||
val request = Request(
|
||||
verb = DELETE,
|
||||
room = null,
|
||||
server = server,
|
||||
endpoint = Endpoint.Inbox
|
||||
)
|
||||
return getResponseBody(request).map { response ->
|
||||
JsonUtil.fromJson(response, Map::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
}
|
@@ -8,6 +8,7 @@ import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.sending_receiving.MessageReceiver.Error
|
||||
import org.session.libsession.messaging.utilities.SessionId
|
||||
import org.session.libsession.messaging.utilities.SodiumUtilities
|
||||
import org.session.libsession.messaging.utilities.SodiumUtilities.sodium
|
||||
import org.session.libsignal.crypto.ecc.ECKeyPair
|
||||
import org.session.libsignal.utilities.Hex
|
||||
import org.session.libsignal.utilities.IdPrefix
|
||||
@@ -17,8 +18,6 @@ import org.session.libsignal.utilities.removingIdPrefixIfNeeded
|
||||
|
||||
object MessageDecrypter {
|
||||
|
||||
private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) }
|
||||
|
||||
/**
|
||||
* Decrypts `ciphertext` using the Session protocol and `x25519KeyPair`.
|
||||
*
|
||||
|
@@ -7,6 +7,7 @@ import com.goterl.lazysodium.interfaces.Sign
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.sending_receiving.MessageSender.Error
|
||||
import org.session.libsession.messaging.utilities.SodiumUtilities
|
||||
import org.session.libsession.messaging.utilities.SodiumUtilities.sodium
|
||||
import org.session.libsignal.utilities.Hex
|
||||
import org.session.libsignal.utilities.IdPrefix
|
||||
import org.session.libsignal.utilities.Log
|
||||
@@ -14,8 +15,6 @@ import org.session.libsignal.utilities.removingIdPrefixIfNeeded
|
||||
|
||||
object MessageEncrypter {
|
||||
|
||||
private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) }
|
||||
|
||||
/**
|
||||
* Encrypts `plaintext` using the Session protocol for `hexEncodedX25519PublicKey`.
|
||||
*
|
||||
|
@@ -9,11 +9,13 @@ import org.session.libsession.messaging.messages.control.DataExtractionNotificat
|
||||
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate
|
||||
import org.session.libsession.messaging.messages.control.MessageRequestResponse
|
||||
import org.session.libsession.messaging.messages.control.ReadReceipt
|
||||
import org.session.libsession.messaging.messages.control.SharedConfigurationMessage
|
||||
import org.session.libsession.messaging.messages.control.TypingIndicator
|
||||
import org.session.libsession.messaging.messages.control.UnsendRequest
|
||||
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
||||
import org.session.libsession.messaging.utilities.SessionId
|
||||
import org.session.libsession.messaging.utilities.SodiumUtilities
|
||||
import org.session.libsession.snode.SnodeAPI
|
||||
import org.session.libsignal.crypto.PushTransportDetails
|
||||
import org.session.libsignal.protos.SignalServiceProtos
|
||||
import org.session.libsignal.utilities.IdPrefix
|
||||
@@ -33,13 +35,16 @@ object MessageReceiver {
|
||||
object NoThread: Error("Couldn't find thread for message.")
|
||||
object SelfSend: Error("Message addressed at self.")
|
||||
object InvalidGroupPublicKey: Error("Invalid group public key.")
|
||||
object NoGroupThread: Error("No thread exists for this group.")
|
||||
object NoGroupKeyPair: Error("Missing group key pair.")
|
||||
object NoUserED25519KeyPair : Error("Couldn't find user ED25519 key pair.")
|
||||
object ExpiredMessage: Error("Message has already expired, prevent adding")
|
||||
|
||||
internal val isRetryable: Boolean = when (this) {
|
||||
is DuplicateMessage, is InvalidMessage, is UnknownMessage,
|
||||
is UnknownEnvelopeType, is InvalidSignature, is NoData,
|
||||
is SenderBlocked, is SelfSend -> false
|
||||
is SenderBlocked, is SelfSend,
|
||||
is ExpiredMessage, is NoGroupThread -> false
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
@@ -50,6 +55,7 @@ object MessageReceiver {
|
||||
isOutgoing: Boolean? = null,
|
||||
otherBlindedPublicKey: String? = null,
|
||||
openGroupPublicKey: String? = null,
|
||||
currentClosedGroups: Set<String>?
|
||||
): Pair<Message, SignalServiceProtos.Content> {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val userPublicKey = storage.getUserPublicKey()
|
||||
@@ -69,7 +75,7 @@ object MessageReceiver {
|
||||
} else {
|
||||
when (envelope.type) {
|
||||
SignalServiceProtos.Envelope.Type.SESSION_MESSAGE -> {
|
||||
if (IdPrefix.fromValue(envelope.source) == IdPrefix.BLINDED) {
|
||||
if (IdPrefix.fromValue(envelope.source)?.isBlinded() == true) {
|
||||
openGroupPublicKey ?: throw Error.InvalidGroupPublicKey
|
||||
otherBlindedPublicKey ?: throw Error.DecryptionFailed
|
||||
val decryptionResult = MessageDecrypter.decryptBlinded(
|
||||
@@ -138,13 +144,16 @@ object MessageReceiver {
|
||||
UnsendRequest.fromProto(proto) ?:
|
||||
MessageRequestResponse.fromProto(proto) ?:
|
||||
CallMessage.fromProto(proto) ?:
|
||||
VisibleMessage.fromProto(proto) ?: run {
|
||||
throw Error.UnknownMessage
|
||||
}
|
||||
SharedConfigurationMessage.fromProto(proto) ?:
|
||||
VisibleMessage.fromProto(proto) ?: throw Error.UnknownMessage
|
||||
|
||||
val isUserBlindedSender = sender == openGroupPublicKey?.let { SodiumUtilities.blindedKeyPair(it, MessagingModuleConfiguration.shared.getUserED25519KeyPair()!!) }?.let { SessionId(IdPrefix.BLINDED, it.publicKey.asBytes).hexString }
|
||||
// Ignore self send if needed
|
||||
if (!message.isSelfSendValid && (sender == userPublicKey || isUserBlindedSender)) {
|
||||
throw Error.SelfSend
|
||||
val isUserSender = sender == userPublicKey
|
||||
|
||||
if (isUserSender || isUserBlindedSender) {
|
||||
// Ignore self send if needed
|
||||
if (!message.isSelfSendValid) throw Error.SelfSend
|
||||
message.isSenderSelf = true
|
||||
}
|
||||
// Guard against control messages in open groups
|
||||
if (isOpenGroupMessage && message !is VisibleMessage) {
|
||||
@@ -154,7 +163,7 @@ object MessageReceiver {
|
||||
message.sender = sender
|
||||
message.recipient = userPublicKey
|
||||
message.sentTimestamp = envelope.timestamp
|
||||
message.receivedTimestamp = if (envelope.hasServerTimestamp()) envelope.serverTimestamp else System.currentTimeMillis()
|
||||
message.receivedTimestamp = if (envelope.hasServerTimestamp()) envelope.serverTimestamp else SnodeAPI.nowWithOffset
|
||||
message.groupPublicKey = groupPublicKey
|
||||
message.openGroupServerMessageID = openGroupServerID
|
||||
// Validate
|
||||
@@ -166,12 +175,16 @@ object MessageReceiver {
|
||||
// If the message failed to process the first time around we retry it later (if the error is retryable). In this case the timestamp
|
||||
// will already be in the database but we don't want to treat the message as a duplicate. The isRetry flag is a simple workaround
|
||||
// for this issue.
|
||||
if (message is ClosedGroupControlMessage && message.kind is ClosedGroupControlMessage.Kind.New) {
|
||||
if (groupPublicKey != null && groupPublicKey !in (currentClosedGroups ?: emptySet())) {
|
||||
throw Error.NoGroupThread
|
||||
}
|
||||
if ((message is ClosedGroupControlMessage && message.kind is ClosedGroupControlMessage.Kind.New) || message is SharedConfigurationMessage) {
|
||||
// Allow duplicates in this case to avoid the following situation:
|
||||
// • The app performed a background poll or received a push notification
|
||||
// • This method was invoked and the received message timestamps table was updated
|
||||
// • Processing wasn't finished
|
||||
// • The user doesn't see the new closed group
|
||||
// also allow shared configuration messages to be duplicates since we track hashes separately use seqno for conflict resolution
|
||||
} else {
|
||||
if (storage.isDuplicateMessage(envelope.timestamp)) { throw Error.DuplicateMessage }
|
||||
storage.addReceivedMessageTimestamp(envelope.timestamp)
|
||||
@@ -179,4 +192,5 @@ object MessageReceiver {
|
||||
// Return
|
||||
return Pair(message, proto)
|
||||
}
|
||||
|
||||
}
|
@@ -1,5 +1,6 @@
|
||||
package org.session.libsession.messaging.sending_receiving
|
||||
|
||||
import network.loki.messenger.libsession_util.util.ExpiryMode
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.deferred
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
@@ -8,13 +9,15 @@ import org.session.libsession.messaging.jobs.MessageSendJob
|
||||
import org.session.libsession.messaging.jobs.NotifyPNServerJob
|
||||
import org.session.libsession.messaging.messages.Destination
|
||||
import org.session.libsession.messaging.messages.Message
|
||||
import org.session.libsession.messaging.messages.applyExpiryMode
|
||||
import org.session.libsession.messaging.messages.control.CallMessage
|
||||
import org.session.libsession.messaging.messages.control.ClosedGroupControlMessage
|
||||
import org.session.libsession.messaging.messages.control.ConfigurationMessage
|
||||
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate
|
||||
import org.session.libsession.messaging.messages.control.MessageRequestResponse
|
||||
import org.session.libsession.messaging.messages.control.SharedConfigurationMessage
|
||||
import org.session.libsession.messaging.messages.control.UnsendRequest
|
||||
import org.session.libsession.messaging.messages.visible.LinkPreview
|
||||
import org.session.libsession.messaging.messages.visible.Profile
|
||||
import org.session.libsession.messaging.messages.visible.Quote
|
||||
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
||||
import org.session.libsession.messaging.open_groups.OpenGroupApi
|
||||
@@ -25,15 +28,18 @@ import org.session.libsession.messaging.utilities.SessionId
|
||||
import org.session.libsession.messaging.utilities.SodiumUtilities
|
||||
import org.session.libsession.snode.RawResponsePromise
|
||||
import org.session.libsession.snode.SnodeAPI
|
||||
import org.session.libsession.snode.SnodeAPI.nowWithOffset
|
||||
import org.session.libsession.snode.SnodeMessage
|
||||
import org.session.libsession.snode.SnodeModule
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.Device
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsession.utilities.SSKEnvironment
|
||||
import org.session.libsignal.crypto.PushTransportDetails
|
||||
import org.session.libsignal.protos.SignalServiceProtos
|
||||
import org.session.libsignal.utilities.Base64
|
||||
import org.session.libsignal.utilities.IdPrefix
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.session.libsignal.utilities.Namespace
|
||||
import org.session.libsignal.utilities.defaultRequiresAuth
|
||||
import org.session.libsignal.utilities.hasNamespaces
|
||||
@@ -66,115 +72,138 @@ object MessageSender {
|
||||
}
|
||||
|
||||
// Convenience
|
||||
fun send(message: Message, destination: Destination): Promise<Unit, Exception> {
|
||||
fun send(message: Message, destination: Destination, isSyncMessage: Boolean): Promise<Unit, Exception> {
|
||||
if (message is VisibleMessage) MessagingModuleConfiguration.shared.lastSentTimestampCache.submitTimestamp(message.threadID!!, message.sentTimestamp!!)
|
||||
return if (destination is Destination.LegacyOpenGroup || destination is Destination.OpenGroup || destination is Destination.OpenGroupInbox) {
|
||||
sendToOpenGroupDestination(destination, message)
|
||||
} else {
|
||||
sendToSnodeDestination(destination, message)
|
||||
sendToSnodeDestination(destination, message, isSyncMessage)
|
||||
}
|
||||
}
|
||||
|
||||
// One-on-One Chats & Closed Groups
|
||||
@Throws(Exception::class)
|
||||
fun buildWrappedMessageToSnode(destination: Destination, message: Message, isSyncMessage: Boolean): SnodeMessage {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val userPublicKey = storage.getUserPublicKey()
|
||||
// Set the timestamp, sender and recipient
|
||||
val messageSendTime = nowWithOffset
|
||||
if (message.sentTimestamp == null) {
|
||||
message.sentTimestamp =
|
||||
messageSendTime // Visible messages will already have their sent timestamp set
|
||||
}
|
||||
|
||||
message.sender = userPublicKey
|
||||
// SHARED CONFIG
|
||||
when (destination) {
|
||||
is Destination.Contact -> message.recipient = destination.publicKey
|
||||
is Destination.ClosedGroup -> message.recipient = destination.groupPublicKey
|
||||
else -> throw IllegalStateException("Destination should not be an open group.")
|
||||
}
|
||||
|
||||
val isSelfSend = (message.recipient == userPublicKey)
|
||||
// Validate the message
|
||||
if (!message.isValid()) {
|
||||
throw Error.InvalidMessage
|
||||
}
|
||||
// Stop here if this is a self-send, unless it's:
|
||||
// • a configuration message
|
||||
// • a sync message
|
||||
// • a closed group control message of type `new`
|
||||
var isNewClosedGroupControlMessage = false
|
||||
if (message is ClosedGroupControlMessage && message.kind is ClosedGroupControlMessage.Kind.New) isNewClosedGroupControlMessage =
|
||||
true
|
||||
if (isSelfSend
|
||||
&& message !is ConfigurationMessage
|
||||
&& !isSyncMessage
|
||||
&& !isNewClosedGroupControlMessage
|
||||
&& message !is UnsendRequest
|
||||
&& message !is SharedConfigurationMessage
|
||||
) {
|
||||
throw Error.InvalidMessage
|
||||
}
|
||||
// Attach the user's profile if needed
|
||||
if (message is VisibleMessage) {
|
||||
message.profile = storage.getUserProfile()
|
||||
}
|
||||
if (message is MessageRequestResponse) {
|
||||
message.profile = storage.getUserProfile()
|
||||
}
|
||||
// Convert it to protobuf
|
||||
val proto = message.toProto() ?: throw Error.ProtoConversionFailed
|
||||
// Serialize the protobuf
|
||||
val plaintext = PushTransportDetails.getPaddedMessageBody(proto.toByteArray())
|
||||
// Encrypt the serialized protobuf
|
||||
val ciphertext = when (destination) {
|
||||
is Destination.Contact -> MessageEncrypter.encrypt(plaintext, destination.publicKey)
|
||||
is Destination.ClosedGroup -> {
|
||||
val encryptionKeyPair =
|
||||
MessagingModuleConfiguration.shared.storage.getLatestClosedGroupEncryptionKeyPair(
|
||||
destination.groupPublicKey
|
||||
)!!
|
||||
MessageEncrypter.encrypt(plaintext, encryptionKeyPair.hexEncodedPublicKey)
|
||||
}
|
||||
else -> throw IllegalStateException("Destination should not be open group.")
|
||||
}
|
||||
// Wrap the result
|
||||
val kind: SignalServiceProtos.Envelope.Type
|
||||
val senderPublicKey: String
|
||||
when (destination) {
|
||||
is Destination.Contact -> {
|
||||
kind = SignalServiceProtos.Envelope.Type.SESSION_MESSAGE
|
||||
senderPublicKey = ""
|
||||
}
|
||||
is Destination.ClosedGroup -> {
|
||||
kind = SignalServiceProtos.Envelope.Type.CLOSED_GROUP_MESSAGE
|
||||
senderPublicKey = destination.groupPublicKey
|
||||
}
|
||||
else -> throw IllegalStateException("Destination should not be open group.")
|
||||
}
|
||||
val wrappedMessage = MessageWrapper.wrap(kind, message.sentTimestamp!!, senderPublicKey, ciphertext)
|
||||
val base64EncodedData = Base64.encodeBytes(wrappedMessage)
|
||||
// Send the result
|
||||
return SnodeMessage(
|
||||
message.recipient!!,
|
||||
base64EncodedData,
|
||||
ttl = getSpecifiedTtl(message, isSyncMessage) ?: message.ttl,
|
||||
messageSendTime
|
||||
)
|
||||
}
|
||||
|
||||
// One-on-One Chats & Closed Groups
|
||||
private fun sendToSnodeDestination(destination: Destination, message: Message, isSyncMessage: Boolean = false): Promise<Unit, Exception> {
|
||||
val deferred = deferred<Unit, Exception>()
|
||||
val promise = deferred.promise
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val userPublicKey = storage.getUserPublicKey()
|
||||
// Set the timestamp, sender and recipient
|
||||
if (message.sentTimestamp == null) {
|
||||
message.sentTimestamp = System.currentTimeMillis() // Visible messages will already have their sent timestamp set
|
||||
}
|
||||
|
||||
val messageSendTime = System.currentTimeMillis()
|
||||
// recipient will be set later, so initialize it as a function here
|
||||
val isSelfSend = { message.recipient == userPublicKey }
|
||||
|
||||
message.sender = userPublicKey
|
||||
val isSelfSend = (message.recipient == userPublicKey)
|
||||
// Set the failure handler (need it here already for precondition failure handling)
|
||||
fun handleFailure(error: Exception) {
|
||||
handleFailedMessageSend(message, error)
|
||||
if (destination is Destination.Contact && message is VisibleMessage && !isSelfSend) {
|
||||
handleFailedMessageSend(message, error, isSyncMessage)
|
||||
if (destination is Destination.Contact && message is VisibleMessage && !isSelfSend()) {
|
||||
SnodeModule.shared.broadcaster.broadcast("messageFailed", message.sentTimestamp!!)
|
||||
}
|
||||
deferred.reject(error)
|
||||
}
|
||||
try {
|
||||
when (destination) {
|
||||
is Destination.Contact -> message.recipient = destination.publicKey
|
||||
is Destination.ClosedGroup -> message.recipient = destination.groupPublicKey
|
||||
else -> throw IllegalStateException("Destination should not be an open group.")
|
||||
}
|
||||
// Validate the message
|
||||
if (!message.isValid()) { throw Error.InvalidMessage }
|
||||
// Stop here if this is a self-send, unless it's:
|
||||
// • a configuration message
|
||||
// • a sync message
|
||||
// • a closed group control message of type `new`
|
||||
var isNewClosedGroupControlMessage = false
|
||||
if (message is ClosedGroupControlMessage && message.kind is ClosedGroupControlMessage.Kind.New) isNewClosedGroupControlMessage = true
|
||||
if (isSelfSend && message !is ConfigurationMessage && !isSyncMessage && !isNewClosedGroupControlMessage && message !is UnsendRequest) {
|
||||
handleSuccessfulMessageSend(message, destination)
|
||||
deferred.resolve(Unit)
|
||||
return promise
|
||||
}
|
||||
// Attach the user's profile if needed
|
||||
if (message is VisibleMessage) {
|
||||
val displayName = storage.getUserDisplayName()!!
|
||||
val profileKey = storage.getUserProfileKey()
|
||||
val profilePictureUrl = storage.getUserProfilePictureURL()
|
||||
if (profileKey != null && profilePictureUrl != null) {
|
||||
message.profile = Profile(displayName, profileKey, profilePictureUrl)
|
||||
} else {
|
||||
message.profile = Profile(displayName)
|
||||
}
|
||||
}
|
||||
// Convert it to protobuf
|
||||
val proto = message.toProto() ?: throw Error.ProtoConversionFailed
|
||||
// Serialize the protobuf
|
||||
val plaintext = PushTransportDetails.getPaddedMessageBody(proto.toByteArray())
|
||||
// Encrypt the serialized protobuf
|
||||
val ciphertext = when (destination) {
|
||||
is Destination.Contact -> MessageEncrypter.encrypt(plaintext, destination.publicKey)
|
||||
is Destination.ClosedGroup -> {
|
||||
val encryptionKeyPair = MessagingModuleConfiguration.shared.storage.getLatestClosedGroupEncryptionKeyPair(destination.groupPublicKey)!!
|
||||
MessageEncrypter.encrypt(plaintext, encryptionKeyPair.hexEncodedPublicKey)
|
||||
}
|
||||
else -> throw IllegalStateException("Destination should not be open group.")
|
||||
}
|
||||
// Wrap the result
|
||||
val kind: SignalServiceProtos.Envelope.Type
|
||||
val senderPublicKey: String
|
||||
val snodeMessage = buildWrappedMessageToSnode(destination, message, isSyncMessage)
|
||||
// TODO: this might change in future for config messages
|
||||
val forkInfo = SnodeAPI.forkInfo
|
||||
val namespaces: List<Int> = when {
|
||||
destination is Destination.ClosedGroup
|
||||
&& forkInfo.defaultRequiresAuth() -> listOf(Namespace.UNAUTHENTICATED_CLOSED_GROUP)
|
||||
|
||||
destination is Destination.ClosedGroup
|
||||
&& forkInfo.hasNamespaces() -> listOf(Namespace.UNAUTHENTICATED_CLOSED_GROUP, Namespace.DEFAULT)
|
||||
&& forkInfo.hasNamespaces() -> listOf(
|
||||
Namespace.UNAUTHENTICATED_CLOSED_GROUP,
|
||||
Namespace.DEFAULT
|
||||
)
|
||||
|
||||
else -> listOf(Namespace.DEFAULT)
|
||||
}
|
||||
when (destination) {
|
||||
is Destination.Contact -> {
|
||||
kind = SignalServiceProtos.Envelope.Type.SESSION_MESSAGE
|
||||
senderPublicKey = ""
|
||||
}
|
||||
is Destination.ClosedGroup -> {
|
||||
kind = SignalServiceProtos.Envelope.Type.CLOSED_GROUP_MESSAGE
|
||||
senderPublicKey = destination.groupPublicKey
|
||||
}
|
||||
else -> throw IllegalStateException("Destination should not be open group.")
|
||||
}
|
||||
val wrappedMessage = MessageWrapper.wrap(kind, message.sentTimestamp!!, senderPublicKey, ciphertext)
|
||||
// Send the result
|
||||
if (destination is Destination.Contact && message is VisibleMessage && !isSelfSend) {
|
||||
SnodeModule.shared.broadcaster.broadcast("calculatingPoW", messageSendTime)
|
||||
}
|
||||
val base64EncodedData = Base64.encodeBytes(wrappedMessage)
|
||||
// Send the result
|
||||
val timestamp = messageSendTime + SnodeAPI.clockOffset
|
||||
val snodeMessage = SnodeMessage(message.recipient!!, base64EncodedData, message.ttl, timestamp)
|
||||
if (destination is Destination.Contact && message is VisibleMessage && !isSelfSend) {
|
||||
SnodeModule.shared.broadcaster.broadcast("sendingMessage", messageSendTime)
|
||||
}
|
||||
namespaces.map { namespace -> SnodeAPI.sendMessage(snodeMessage, requiresAuth = false, namespace = namespace) }.let { promises ->
|
||||
var isSuccess = false
|
||||
val promiseCount = promises.size
|
||||
@@ -183,13 +212,23 @@ object MessageSender {
|
||||
promise.success {
|
||||
if (isSuccess) { return@success } // Succeed as soon as the first promise succeeds
|
||||
isSuccess = true
|
||||
if (destination is Destination.Contact && message is VisibleMessage && !isSelfSend) {
|
||||
SnodeModule.shared.broadcaster.broadcast("messageSent", messageSendTime)
|
||||
}
|
||||
val hash = it["hash"] as? String
|
||||
message.serverHash = hash
|
||||
handleSuccessfulMessageSend(message, destination, isSyncMessage)
|
||||
val shouldNotify = ((message is VisibleMessage || message is UnsendRequest || message is CallMessage) && !isSyncMessage)
|
||||
|
||||
val shouldNotify: Boolean = when (message) {
|
||||
is VisibleMessage, is UnsendRequest -> !isSyncMessage
|
||||
is CallMessage -> {
|
||||
// Note: Other 'CallMessage' types are too big to send as push notifications
|
||||
// so only send the 'preOffer' message as a notification
|
||||
when (message.type) {
|
||||
SignalServiceProtos.CallMessage.Type.PRE_OFFER -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
|
||||
/*
|
||||
if (message is ClosedGroupControlMessage && message.kind is ClosedGroupControlMessage.Kind.New) {
|
||||
shouldNotify = true
|
||||
@@ -214,12 +253,32 @@ object MessageSender {
|
||||
return promise
|
||||
}
|
||||
|
||||
private fun getSpecifiedTtl(
|
||||
message: Message,
|
||||
isSyncMessage: Boolean
|
||||
): Long? = message.takeUnless { it is ClosedGroupControlMessage }?.run {
|
||||
threadID ?: (if (isSyncMessage && this is VisibleMessage) syncTarget else recipient)
|
||||
?.let(Address.Companion::fromSerialized)
|
||||
?.let(MessagingModuleConfiguration.shared.storage::getThreadId)
|
||||
}?.let(MessagingModuleConfiguration.shared.storage::getExpirationConfiguration)
|
||||
?.takeIf { it.isEnabled }
|
||||
?.expiryMode
|
||||
?.takeIf { it is ExpiryMode.AfterSend || isSyncMessage }
|
||||
?.expiryMillis
|
||||
|
||||
// Open Groups
|
||||
private fun sendToOpenGroupDestination(destination: Destination, message: Message): Promise<Unit, Exception> {
|
||||
val deferred = deferred<Unit, Exception>()
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val configFactory = MessagingModuleConfiguration.shared.configFactory
|
||||
if (message.sentTimestamp == null) {
|
||||
message.sentTimestamp = System.currentTimeMillis()
|
||||
message.sentTimestamp = nowWithOffset
|
||||
}
|
||||
// Attach the blocks message requests info
|
||||
configFactory.user?.let { user ->
|
||||
if (message is VisibleMessage) {
|
||||
message.blocksMessageRequests = !user.getCommunityMessageRequests()
|
||||
}
|
||||
}
|
||||
val userEdKeyPair = MessagingModuleConfiguration.shared.getUserED25519KeyPair()!!
|
||||
var serverCapabilities = listOf<String>()
|
||||
@@ -257,14 +316,7 @@ object MessageSender {
|
||||
try {
|
||||
// Attach the user's profile if needed
|
||||
if (message is VisibleMessage) {
|
||||
val displayName = storage.getUserDisplayName()!!
|
||||
val profileKey = storage.getUserProfileKey()
|
||||
val profilePictureUrl = storage.getUserProfilePictureURL()
|
||||
if (profileKey != null && profilePictureUrl != null) {
|
||||
message.profile = Profile(displayName, profileKey, profilePictureUrl)
|
||||
} else {
|
||||
message.profile = Profile(displayName)
|
||||
}
|
||||
message.profile = storage.getUserProfile()
|
||||
}
|
||||
when (destination) {
|
||||
is Destination.OpenGroup -> {
|
||||
@@ -320,25 +372,33 @@ object MessageSender {
|
||||
}
|
||||
|
||||
// Result Handling
|
||||
fun handleSuccessfulMessageSend(message: Message, destination: Destination, isSyncMessage: Boolean = false, openGroupSentTimestamp: Long = -1) {
|
||||
private fun handleSuccessfulMessageSend(message: Message, destination: Destination, isSyncMessage: Boolean = false, openGroupSentTimestamp: Long = -1) {
|
||||
if (message is VisibleMessage) MessagingModuleConfiguration.shared.lastSentTimestampCache.submitTimestamp(message.threadID!!, openGroupSentTimestamp)
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val userPublicKey = storage.getUserPublicKey()!!
|
||||
val timestamp = message.sentTimestamp!!
|
||||
// Ignore future self-sends
|
||||
storage.addReceivedMessageTimestamp(message.sentTimestamp!!)
|
||||
storage.getMessageIdInDatabase(message.sentTimestamp!!, userPublicKey)?.let { messageID ->
|
||||
storage.addReceivedMessageTimestamp(timestamp)
|
||||
storage.getMessageIdInDatabase(timestamp, userPublicKey)?.let { (messageID, mms) ->
|
||||
if (openGroupSentTimestamp != -1L && message is VisibleMessage) {
|
||||
storage.addReceivedMessageTimestamp(openGroupSentTimestamp)
|
||||
storage.updateSentTimestamp(messageID, message.isMediaMessage(), openGroupSentTimestamp, message.threadID!!)
|
||||
message.sentTimestamp = openGroupSentTimestamp
|
||||
}
|
||||
|
||||
// When the sync message is successfully sent, the hash value of this TSOutgoingMessage
|
||||
// will be replaced by the hash value of the sync message. Since the hash value of the
|
||||
// real message has no use when we delete a message. It is OK to let it be.
|
||||
message.serverHash?.let {
|
||||
storage.setMessageServerHash(messageID, it)
|
||||
storage.setMessageServerHash(messageID, mms, it)
|
||||
}
|
||||
|
||||
// in case any errors from previous sends
|
||||
storage.clearErrorMessage(messageID)
|
||||
|
||||
// Track the open group server message ID
|
||||
if (message.openGroupServerMessageID != null && (destination is Destination.LegacyOpenGroup || destination is Destination.OpenGroup)) {
|
||||
val messageIsAddressedToCommunity = message.openGroupServerMessageID != null && (destination is Destination.LegacyOpenGroup || destination is Destination.OpenGroup)
|
||||
if (messageIsAddressedToCommunity) {
|
||||
val server: String
|
||||
val room: String
|
||||
when (destination) {
|
||||
@@ -360,13 +420,28 @@ object MessageSender {
|
||||
storage.setOpenGroupServerMessageID(messageID, message.openGroupServerMessageID!!, threadID, !(message as VisibleMessage).isMediaMessage())
|
||||
}
|
||||
}
|
||||
// Mark the message as sent
|
||||
storage.markAsSent(message.sentTimestamp!!, userPublicKey)
|
||||
storage.markUnidentified(message.sentTimestamp!!, userPublicKey)
|
||||
// Start the disappearing messages timer if needed
|
||||
if (message is VisibleMessage && !isSyncMessage) {
|
||||
SSKEnvironment.shared.messageExpirationManager.startAnyExpiration(message.sentTimestamp!!, userPublicKey)
|
||||
|
||||
// Mark the message as sent.
|
||||
// Note: When sending a message to a community the server modifies the message timestamp
|
||||
// so when we go to look up the message in the local database by timestamp it fails and
|
||||
// we're left with the message delivery status as "Sending" forever! As such, we use a
|
||||
// pair of modified "markAsSentToCommunity" and "markUnidentifiedInCommunity" methods
|
||||
// to retrieve the local message by thread & message ID rather than timestamp when
|
||||
// handling community messages only so we can tick the delivery status over to 'Sent'.
|
||||
// Fixed in: https://optf.atlassian.net/browse/SES-1567
|
||||
if (messageIsAddressedToCommunity)
|
||||
{
|
||||
storage.markAsSentToCommunity(message.threadID!!, message.id!!)
|
||||
storage.markUnidentifiedInCommunity(message.threadID!!, message.id!!)
|
||||
}
|
||||
else
|
||||
{
|
||||
storage.markAsSent(timestamp, userPublicKey)
|
||||
storage.markUnidentified(timestamp, userPublicKey)
|
||||
}
|
||||
|
||||
// Start the disappearing messages timer if needed
|
||||
SSKEnvironment.shared.messageExpirationManager.maybeStartExpiration(message, startDisappearAfterRead = true)
|
||||
} ?: run {
|
||||
storage.updateReactionIfNeeded(message, message.sender?:userPublicKey, openGroupSentTimestamp)
|
||||
}
|
||||
@@ -375,16 +450,23 @@ object MessageSender {
|
||||
// • the destination was a contact
|
||||
// • we didn't sync it already
|
||||
if (destination is Destination.Contact && !isSyncMessage) {
|
||||
if (message is VisibleMessage) { message.syncTarget = destination.publicKey }
|
||||
if (message is ExpirationTimerUpdate) { message.syncTarget = destination.publicKey }
|
||||
if (message is VisibleMessage) message.syncTarget = destination.publicKey
|
||||
if (message is ExpirationTimerUpdate) message.syncTarget = destination.publicKey
|
||||
|
||||
storage.markAsSyncing(timestamp, userPublicKey)
|
||||
sendToSnodeDestination(Destination.Contact(userPublicKey), message, true)
|
||||
}
|
||||
}
|
||||
|
||||
fun handleFailedMessageSend(message: Message, error: Exception) {
|
||||
fun handleFailedMessageSend(message: Message, error: Exception, isSyncMessage: Boolean = false) {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val userPublicKey = storage.getUserPublicKey()!!
|
||||
storage.setErrorMessage(message.sentTimestamp!!, message.sender?:userPublicKey, error)
|
||||
|
||||
val timestamp = message.sentTimestamp!!
|
||||
val author = message.sender ?: userPublicKey
|
||||
|
||||
if (isSyncMessage) storage.markAsSyncFailed(timestamp, author, error)
|
||||
else storage.markAsSentFailed(timestamp, author, error)
|
||||
}
|
||||
|
||||
// Convenience
|
||||
@@ -398,7 +480,7 @@ object MessageSender {
|
||||
message.linkPreview?.let { linkPreview ->
|
||||
if (linkPreview.attachmentID == null) {
|
||||
messageDataProvider.getLinkPreviewAttachmentIDFor(message.id!!)?.let { attachmentID ->
|
||||
message.linkPreview!!.attachmentID = attachmentID
|
||||
linkPreview.attachmentID = attachmentID
|
||||
message.attachmentIDs.remove(attachmentID)
|
||||
}
|
||||
}
|
||||
@@ -408,29 +490,30 @@ object MessageSender {
|
||||
|
||||
@JvmStatic
|
||||
fun send(message: Message, address: Address) {
|
||||
val threadID = MessagingModuleConfiguration.shared.storage.getOrCreateThreadIdFor(address)
|
||||
val threadID = MessagingModuleConfiguration.shared.storage.getThreadId(address)
|
||||
threadID?.let(message::applyExpiryMode)
|
||||
message.threadID = threadID
|
||||
val destination = Destination.from(address)
|
||||
val job = MessageSendJob(message, destination)
|
||||
JobQueue.shared.add(job)
|
||||
}
|
||||
|
||||
fun sendNonDurably(message: VisibleMessage, attachments: List<SignalAttachment>, address: Address): Promise<Unit, Exception> {
|
||||
fun sendNonDurably(message: VisibleMessage, attachments: List<SignalAttachment>, address: Address, isSyncMessage: Boolean): Promise<Unit, Exception> {
|
||||
val attachmentIDs = MessagingModuleConfiguration.shared.messageDataProvider.getAttachmentIDsFor(message.id!!)
|
||||
message.attachmentIDs.addAll(attachmentIDs)
|
||||
return sendNonDurably(message, address)
|
||||
return sendNonDurably(message, address, isSyncMessage)
|
||||
}
|
||||
|
||||
fun sendNonDurably(message: Message, address: Address): Promise<Unit, Exception> {
|
||||
val threadID = MessagingModuleConfiguration.shared.storage.getOrCreateThreadIdFor(address)
|
||||
fun sendNonDurably(message: Message, address: Address, isSyncMessage: Boolean): Promise<Unit, Exception> {
|
||||
val threadID = MessagingModuleConfiguration.shared.storage.getThreadId(address)
|
||||
message.threadID = threadID
|
||||
val destination = Destination.from(address)
|
||||
return send(message, destination)
|
||||
return send(message, destination, isSyncMessage)
|
||||
}
|
||||
|
||||
// Closed groups
|
||||
fun createClosedGroup(name: String, members: Collection<String>): Promise<String, Exception> {
|
||||
return create(name, members)
|
||||
fun createClosedGroup(device: Device, name: String, members: Collection<String>): Promise<String, Exception> {
|
||||
return create(device, name, members)
|
||||
}
|
||||
|
||||
fun explicitNameChange(groupPublicKey: String, newName: String) {
|
||||
|
@@ -8,31 +8,36 @@ import nl.komponents.kovenant.deferred
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.messages.control.ClosedGroupControlMessage
|
||||
import org.session.libsession.messaging.sending_receiving.MessageSender.Error
|
||||
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI
|
||||
import org.session.libsession.messaging.sending_receiving.notifications.PushRegistryV1
|
||||
import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPollerV2
|
||||
import org.session.libsession.snode.SnodeAPI
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.Address.Companion.fromSerialized
|
||||
import org.session.libsession.utilities.Device
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.session.libsignal.crypto.ecc.Curve
|
||||
import org.session.libsignal.crypto.ecc.ECKeyPair
|
||||
import org.session.libsignal.utilities.guava.Optional
|
||||
import org.session.libsignal.messages.SignalServiceGroup
|
||||
import org.session.libsignal.protos.SignalServiceProtos
|
||||
import org.session.libsignal.utilities.Hex
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.session.libsignal.utilities.ThreadUtils
|
||||
import org.session.libsignal.utilities.guava.Optional
|
||||
import org.session.libsignal.utilities.hexEncodedPublicKey
|
||||
import org.session.libsignal.utilities.removingIdPrefixIfNeeded
|
||||
import org.session.libsignal.utilities.Hex
|
||||
import org.session.libsignal.utilities.ThreadUtils
|
||||
import org.session.libsignal.utilities.Log
|
||||
import java.util.*
|
||||
import java.util.LinkedList
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
const val groupSizeLimit = 100
|
||||
|
||||
val pendingKeyPairs = ConcurrentHashMap<String, Optional<ECKeyPair>>()
|
||||
|
||||
fun MessageSender.create(name: String, members: Collection<String>): Promise<String, Exception> {
|
||||
fun MessageSender.create(
|
||||
device: Device,
|
||||
name: String,
|
||||
members: Collection<String>
|
||||
): Promise<String, Exception> {
|
||||
val deferred = deferred<String, Exception>()
|
||||
ThreadUtils.queue {
|
||||
// Prepare
|
||||
@@ -48,32 +53,47 @@ fun MessageSender.create(name: String, members: Collection<String>): Promise<Str
|
||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||
val admins = setOf( userPublicKey )
|
||||
val adminsAsData = admins.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) }
|
||||
storage.createGroup(groupID, name, LinkedList(members.map { Address.fromSerialized(it) }),
|
||||
null, null, LinkedList(admins.map { Address.fromSerialized(it) }), System.currentTimeMillis())
|
||||
storage.createGroup(groupID, name, LinkedList(members.map { fromSerialized(it) }),
|
||||
null, null, LinkedList(admins.map { Address.fromSerialized(it) }), SnodeAPI.nowWithOffset)
|
||||
storage.setProfileSharing(Address.fromSerialized(groupID), true)
|
||||
|
||||
// Send a closed group update message to all members individually
|
||||
val closedGroupUpdateKind = ClosedGroupControlMessage.Kind.New(ByteString.copyFrom(Hex.fromStringCondensed(groupPublicKey)), name, encryptionKeyPair, membersAsData, adminsAsData, 0)
|
||||
val sentTime = System.currentTimeMillis()
|
||||
val sentTime = SnodeAPI.nowWithOffset
|
||||
|
||||
// Add the group to the user's set of public keys to poll for
|
||||
storage.addClosedGroupPublicKey(groupPublicKey)
|
||||
// Store the encryption key pair
|
||||
storage.addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey, sentTime)
|
||||
// Create the thread
|
||||
storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||
|
||||
// Notify the user
|
||||
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||
storage.insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.CREATION, name, members, admins, threadID, sentTime)
|
||||
|
||||
val ourPubKey = storage.getUserPublicKey()
|
||||
for (member in members) {
|
||||
val closedGroupControlMessage = ClosedGroupControlMessage(closedGroupUpdateKind)
|
||||
val closedGroupControlMessage = ClosedGroupControlMessage(closedGroupUpdateKind, groupID)
|
||||
closedGroupControlMessage.sentTimestamp = sentTime
|
||||
try {
|
||||
sendNonDurably(closedGroupControlMessage, Address.fromSerialized(member)).get()
|
||||
sendNonDurably(closedGroupControlMessage, Address.fromSerialized(member), member == ourPubKey).get()
|
||||
} catch (e: Exception) {
|
||||
// We failed to properly create the group so delete it's associated data (in the past
|
||||
// we didn't create this data until the messages successfully sent but this resulted
|
||||
// in race conditions due to the `NEW` message sent to our own swarm)
|
||||
storage.removeClosedGroupPublicKey(groupPublicKey)
|
||||
storage.removeAllClosedGroupEncryptionKeyPairs(groupPublicKey)
|
||||
storage.deleteConversation(threadID)
|
||||
deferred.reject(e)
|
||||
return@queue
|
||||
}
|
||||
}
|
||||
|
||||
// Add the group to the user's set of public keys to poll for
|
||||
storage.addClosedGroupPublicKey(groupPublicKey)
|
||||
// Store the encryption key pair
|
||||
storage.addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey)
|
||||
// Notify the user
|
||||
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||
storage.insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.CREATION, name, members, admins, threadID, sentTime)
|
||||
// Add the group to the config now that it was successfully created
|
||||
storage.createInitialConfigGroup(groupPublicKey, name, GroupUtil.createConfigMemberMap(members, admins), sentTime, encryptionKeyPair, 0)
|
||||
// Notify the PN server
|
||||
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey)
|
||||
PushRegistryV1.register(device = device, publicKey = userPublicKey)
|
||||
// Start polling
|
||||
ClosedGroupPollerV2.shared.startPolling(groupPublicKey)
|
||||
// Fulfill the promise
|
||||
@@ -83,24 +103,6 @@ fun MessageSender.create(name: String, members: Collection<String>): Promise<Str
|
||||
return deferred.promise
|
||||
}
|
||||
|
||||
fun MessageSender.update(groupPublicKey: String, members: List<String>, name: String) {
|
||||
val context = MessagingModuleConfiguration.shared.context
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||
val group = storage.getGroup(groupID) ?: run {
|
||||
Log.d("Loki", "Can't update nonexistent closed group.")
|
||||
throw Error.NoThread
|
||||
}
|
||||
// Update name if needed
|
||||
if (name != group.title) { setName(groupPublicKey, name) }
|
||||
// Add members if needed
|
||||
val addedMembers = members - group.members.map { it.serialize() }
|
||||
if (!addedMembers.isEmpty()) { addMembers(groupPublicKey, addedMembers) }
|
||||
// Remove members if needed
|
||||
val removedMembers = group.members.map { it.serialize() } - members
|
||||
if (removedMembers.isEmpty()) { removeMembers(groupPublicKey, removedMembers) }
|
||||
}
|
||||
|
||||
fun MessageSender.setName(groupPublicKey: String, newName: String) {
|
||||
val context = MessagingModuleConfiguration.shared.context
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
@@ -113,8 +115,8 @@ fun MessageSender.setName(groupPublicKey: String, newName: String) {
|
||||
val admins = group.admins.map { it.serialize() }
|
||||
// Send the update to the group
|
||||
val kind = ClosedGroupControlMessage.Kind.NameChange(newName)
|
||||
val sentTime = System.currentTimeMillis()
|
||||
val closedGroupControlMessage = ClosedGroupControlMessage(kind)
|
||||
val sentTime = SnodeAPI.nowWithOffset
|
||||
val closedGroupControlMessage = ClosedGroupControlMessage(kind, groupID)
|
||||
closedGroupControlMessage.sentTimestamp = sentTime
|
||||
send(closedGroupControlMessage, Address.fromSerialized(groupID))
|
||||
// Update the group
|
||||
@@ -133,8 +135,8 @@ fun MessageSender.addMembers(groupPublicKey: String, membersToAdd: List<String>)
|
||||
Log.d("Loki", "Can't add members to nonexistent closed group.")
|
||||
throw Error.NoThread
|
||||
}
|
||||
val recipient = Recipient.from(context, fromSerialized(groupID), false)
|
||||
val expireTimer = recipient.expireMessages
|
||||
val threadId = storage.getOrCreateThreadIdFor(fromSerialized(groupID))
|
||||
val expireTimer = storage.getExpirationConfiguration(threadId)?.expiryMode?.expirySeconds ?: 0
|
||||
if (membersToAdd.isEmpty()) {
|
||||
Log.d("Loki", "Invalid closed group update.")
|
||||
throw Error.InvalidClosedGroupUpdate
|
||||
@@ -153,21 +155,28 @@ fun MessageSender.addMembers(groupPublicKey: String, membersToAdd: List<String>)
|
||||
val name = group.title
|
||||
// Send the update to the group
|
||||
val memberUpdateKind = ClosedGroupControlMessage.Kind.MembersAdded(newMembersAsData)
|
||||
val sentTime = System.currentTimeMillis()
|
||||
val closedGroupControlMessage = ClosedGroupControlMessage(memberUpdateKind)
|
||||
val sentTime = SnodeAPI.nowWithOffset
|
||||
val closedGroupControlMessage = ClosedGroupControlMessage(memberUpdateKind, groupID)
|
||||
closedGroupControlMessage.sentTimestamp = sentTime
|
||||
send(closedGroupControlMessage, Address.fromSerialized(groupID))
|
||||
// Send closed group update messages to any new members individually
|
||||
for (member in membersToAdd) {
|
||||
val closedGroupNewKind = ClosedGroupControlMessage.Kind.New(ByteString.copyFrom(Hex.fromStringCondensed(groupPublicKey)), name, encryptionKeyPair, membersAsData, adminsAsData, expireTimer)
|
||||
val closedGroupControlMessage = ClosedGroupControlMessage(closedGroupNewKind)
|
||||
val closedGroupNewKind = ClosedGroupControlMessage.Kind.New(
|
||||
ByteString.copyFrom(Hex.fromStringCondensed(groupPublicKey)),
|
||||
name,
|
||||
encryptionKeyPair,
|
||||
membersAsData,
|
||||
adminsAsData,
|
||||
expireTimer.toInt()
|
||||
)
|
||||
val closedGroupControlMessage = ClosedGroupControlMessage(closedGroupNewKind, groupID)
|
||||
// It's important that the sent timestamp of this message is greater than the sent timestamp
|
||||
// of the `MembersAdded` message above. The reason is that upon receiving this `New` message,
|
||||
// the recipient will update the closed group formation timestamp and ignore any closed group
|
||||
// updates from before that timestamp. By setting the timestamp of the message below to a value
|
||||
// greater than that of the `MembersAdded` message, we ensure that newly added members ignore
|
||||
// the `MembersAdded` message.
|
||||
closedGroupControlMessage.sentTimestamp = System.currentTimeMillis()
|
||||
closedGroupControlMessage.sentTimestamp = SnodeAPI.nowWithOffset
|
||||
send(closedGroupControlMessage, Address.fromSerialized(member))
|
||||
}
|
||||
// Notify the user
|
||||
@@ -208,8 +217,8 @@ fun MessageSender.removeMembers(groupPublicKey: String, membersToRemove: List<St
|
||||
val name = group.title
|
||||
// Send the update to the group
|
||||
val memberUpdateKind = ClosedGroupControlMessage.Kind.MembersRemoved(removeMembersAsData)
|
||||
val sentTime = System.currentTimeMillis()
|
||||
val closedGroupControlMessage = ClosedGroupControlMessage(memberUpdateKind)
|
||||
val sentTime = SnodeAPI.nowWithOffset
|
||||
val closedGroupControlMessage = ClosedGroupControlMessage(memberUpdateKind, groupID)
|
||||
closedGroupControlMessage.sentTimestamp = sentTime
|
||||
send(closedGroupControlMessage, Address.fromSerialized(groupID))
|
||||
// Send the new encryption key pair to the remaining group members.
|
||||
@@ -238,19 +247,19 @@ fun MessageSender.leave(groupPublicKey: String, notifyUser: Boolean = true): Pro
|
||||
val admins = group.admins.map { it.serialize() }
|
||||
val name = group.title
|
||||
// Send the update to the group
|
||||
val closedGroupControlMessage = ClosedGroupControlMessage(ClosedGroupControlMessage.Kind.MemberLeft())
|
||||
val sentTime = System.currentTimeMillis()
|
||||
val closedGroupControlMessage = ClosedGroupControlMessage(ClosedGroupControlMessage.Kind.MemberLeft(), groupID)
|
||||
val sentTime = SnodeAPI.nowWithOffset
|
||||
closedGroupControlMessage.sentTimestamp = sentTime
|
||||
storage.setActive(groupID, false)
|
||||
sendNonDurably(closedGroupControlMessage, Address.fromSerialized(groupID)).success {
|
||||
sendNonDurably(closedGroupControlMessage, Address.fromSerialized(groupID), isSyncMessage = false).success {
|
||||
// Notify the user
|
||||
val infoType = SignalServiceGroup.Type.QUIT
|
||||
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||
if (notifyUser) {
|
||||
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||
storage.insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID, sentTime)
|
||||
}
|
||||
// Remove the group private key and unsubscribe from PNs
|
||||
MessageReceiver.disableLocalGroupAndUnsubscribe(groupPublicKey, groupID, userPublicKey)
|
||||
MessageReceiver.disableLocalGroupAndUnsubscribe(groupPublicKey, groupID, userPublicKey, true)
|
||||
deferred.resolve(Unit)
|
||||
}.fail {
|
||||
storage.setActive(groupID, true)
|
||||
@@ -282,7 +291,7 @@ fun MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey: String, ta
|
||||
// Distribute it
|
||||
sendEncryptionKeyPair(groupPublicKey, newKeyPair, targetMembers)?.success {
|
||||
// Store it * after * having sent out the message to the group
|
||||
storage.addClosedGroupEncryptionKeyPair(newKeyPair, groupPublicKey)
|
||||
storage.addClosedGroupEncryptionKeyPair(newKeyPair, groupPublicKey, SnodeAPI.nowWithOffset)
|
||||
pendingKeyPairs[groupPublicKey] = Optional.absent()
|
||||
}
|
||||
}
|
||||
@@ -298,11 +307,12 @@ fun MessageSender.sendEncryptionKeyPair(groupPublicKey: String, newKeyPair: ECKe
|
||||
ClosedGroupControlMessage.KeyPairWrapper(publicKey, ByteString.copyFrom(ciphertext))
|
||||
}
|
||||
val kind = ClosedGroupControlMessage.Kind.EncryptionKeyPair(ByteString.copyFrom(Hex.fromStringCondensed(groupPublicKey)), wrappers)
|
||||
val sentTime = System.currentTimeMillis()
|
||||
val closedGroupControlMessage = ClosedGroupControlMessage(kind)
|
||||
val sentTime = SnodeAPI.nowWithOffset
|
||||
val closedGroupControlMessage = ClosedGroupControlMessage(kind, null)
|
||||
closedGroupControlMessage.sentTimestamp = sentTime
|
||||
return if (force) {
|
||||
MessageSender.sendNonDurably(closedGroupControlMessage, Address.fromSerialized(destination))
|
||||
val isSync = MessagingModuleConfiguration.shared.storage.getUserPublicKey() == destination
|
||||
MessageSender.sendNonDurably(closedGroupControlMessage, Address.fromSerialized(destination), isSyncMessage = isSync)
|
||||
} else {
|
||||
MessageSender.send(closedGroupControlMessage, Address.fromSerialized(destination))
|
||||
null
|
||||
@@ -333,6 +343,6 @@ fun MessageSender.sendLatestEncryptionKeyPair(publicKey: String, groupPublicKey:
|
||||
Log.d("Loki", "Sending latest encryption key pair to: $publicKey.")
|
||||
val wrapper = ClosedGroupControlMessage.KeyPairWrapper(publicKey, ByteString.copyFrom(ciphertext))
|
||||
val kind = ClosedGroupControlMessage.Kind.EncryptionKeyPair(ByteString.copyFrom(Hex.fromStringCondensed(groupPublicKey)), listOf(wrapper))
|
||||
val closedGroupControlMessage = ClosedGroupControlMessage(kind)
|
||||
val closedGroupControlMessage = ClosedGroupControlMessage(kind, groupID)
|
||||
MessageSender.send(closedGroupControlMessage, Address.fromSerialized(publicKey))
|
||||
}
|
@@ -1,10 +1,14 @@
|
||||
package org.session.libsession.messaging.sending_receiving
|
||||
|
||||
import android.text.TextUtils
|
||||
import network.loki.messenger.libsession_util.ConfigBase
|
||||
import network.loki.messenger.libsession_util.util.ExpiryMode
|
||||
import org.session.libsession.avatars.AvatarHelper
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.jobs.BackgroundGroupAddJob
|
||||
import org.session.libsession.messaging.jobs.JobQueue
|
||||
import org.session.libsession.messaging.messages.ExpirationConfiguration
|
||||
import org.session.libsession.messaging.messages.ExpirationConfiguration.Companion.isNewConfigEnabled
|
||||
import org.session.libsession.messaging.messages.Message
|
||||
import org.session.libsession.messaging.messages.control.CallMessage
|
||||
import org.session.libsession.messaging.messages.control.ClosedGroupControlMessage
|
||||
@@ -22,7 +26,7 @@ import org.session.libsession.messaging.open_groups.OpenGroupApi
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.PointerAttachment
|
||||
import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage
|
||||
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
|
||||
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI
|
||||
import org.session.libsession.messaging.sending_receiving.notifications.PushRegistryV1
|
||||
import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPollerV2
|
||||
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel
|
||||
import org.session.libsession.messaging.utilities.SessionId
|
||||
@@ -30,8 +34,10 @@ import org.session.libsession.messaging.utilities.SodiumUtilities
|
||||
import org.session.libsession.messaging.utilities.WebRtcUtils
|
||||
import org.session.libsession.snode.SnodeAPI
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.Address.Companion.fromSerialized
|
||||
import org.session.libsession.utilities.GroupRecord
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsession.utilities.GroupUtil.doubleEncodeGroupID
|
||||
import org.session.libsession.utilities.ProfileKeyUtil
|
||||
import org.session.libsession.utilities.SSKEnvironment
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
@@ -41,6 +47,7 @@ import org.session.libsignal.crypto.ecc.DjbECPublicKey
|
||||
import org.session.libsignal.crypto.ecc.ECKeyPair
|
||||
import org.session.libsignal.messages.SignalServiceGroup
|
||||
import org.session.libsignal.protos.SignalServiceProtos
|
||||
import org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage
|
||||
import org.session.libsignal.utilities.Base64
|
||||
import org.session.libsignal.utilities.IdPrefix
|
||||
import org.session.libsignal.utilities.Log
|
||||
@@ -57,7 +64,10 @@ internal fun MessageReceiver.isBlocked(publicKey: String): Boolean {
|
||||
return recipient.isBlocked
|
||||
}
|
||||
|
||||
fun MessageReceiver.handle(message: Message, proto: SignalServiceProtos.Content, openGroupID: String?) {
|
||||
fun MessageReceiver.handle(message: Message, proto: SignalServiceProtos.Content, threadId: Long, openGroupID: String?) {
|
||||
// Do nothing if the message was outdated
|
||||
if (MessageReceiver.messageIsOutdated(message, threadId, openGroupID)) { return }
|
||||
|
||||
when (message) {
|
||||
is ReadReceipt -> handleReadReceipt(message)
|
||||
is TypingIndicator -> handleTypingIndicator(message)
|
||||
@@ -67,8 +77,8 @@ fun MessageReceiver.handle(message: Message, proto: SignalServiceProtos.Content,
|
||||
is ConfigurationMessage -> handleConfigurationMessage(message)
|
||||
is UnsendRequest -> handleUnsendRequest(message)
|
||||
is MessageRequestResponse -> handleMessageRequestResponse(message)
|
||||
is VisibleMessage -> handleVisibleMessage(message, proto, openGroupID,
|
||||
runIncrement = true,
|
||||
is VisibleMessage -> handleVisibleMessage(
|
||||
message, proto, openGroupID, threadId,
|
||||
runThreadUpdate = true,
|
||||
runProfileUpdate = true
|
||||
)
|
||||
@@ -76,6 +86,33 @@ fun MessageReceiver.handle(message: Message, proto: SignalServiceProtos.Content,
|
||||
}
|
||||
}
|
||||
|
||||
fun MessageReceiver.messageIsOutdated(message: Message, threadId: Long, openGroupID: String?): Boolean {
|
||||
when (message) {
|
||||
is ReadReceipt -> return false // No visible artifact created so better to keep for more reliable read states
|
||||
is UnsendRequest -> return false // We should always process the removal of messages just in case
|
||||
}
|
||||
|
||||
// Determine the state of the conversation and the validity of the message
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val userPublicKey = storage.getUserPublicKey()!!
|
||||
val threadRecipient = storage.getRecipientForThread(threadId)
|
||||
val conversationVisibleInConfig = storage.conversationInConfig(
|
||||
if (message.groupPublicKey == null) threadRecipient?.address?.serialize() else null,
|
||||
message.groupPublicKey,
|
||||
openGroupID,
|
||||
true
|
||||
)
|
||||
val canPerformChange = storage.canPerformConfigChange(
|
||||
if (threadRecipient?.address?.serialize() == userPublicKey) SharedConfigMessage.Kind.USER_PROFILE.name else SharedConfigMessage.Kind.CONTACTS.name,
|
||||
userPublicKey,
|
||||
message.sentTimestamp!!
|
||||
)
|
||||
|
||||
// If the thread is visible or the message was sent more recently than the last config message (minus
|
||||
// buffer period) then we should process the message, if not then the message is outdated
|
||||
return (!conversationVisibleInConfig && !canPerformChange)
|
||||
}
|
||||
|
||||
// region Control Messages
|
||||
private fun MessageReceiver.handleReadReceipt(message: ReadReceipt) {
|
||||
val context = MessagingModuleConfiguration.shared.context
|
||||
@@ -116,10 +153,27 @@ fun MessageReceiver.cancelTypingIndicatorsIfNeeded(senderPublicKey: String) {
|
||||
}
|
||||
|
||||
private fun MessageReceiver.handleExpirationTimerUpdate(message: ExpirationTimerUpdate) {
|
||||
if (message.duration!! > 0) {
|
||||
SSKEnvironment.shared.messageExpirationManager.setExpirationTimer(message)
|
||||
} else {
|
||||
SSKEnvironment.shared.messageExpirationManager.disableExpirationTimer(message)
|
||||
SSKEnvironment.shared.messageExpirationManager.insertExpirationTimerMessage(message)
|
||||
|
||||
// TODO (Groups V2 - FIXME)
|
||||
val isGroupV1 = message.groupPublicKey != null
|
||||
|
||||
if (isNewConfigEnabled && !isGroupV1) return
|
||||
|
||||
val module = MessagingModuleConfiguration.shared
|
||||
try {
|
||||
val threadId = fromSerialized(message.groupPublicKey?.let(::doubleEncodeGroupID) ?: message.sender!!)
|
||||
.let(module.storage::getOrCreateThreadIdFor)
|
||||
|
||||
module.storage.setExpirationConfiguration(
|
||||
ExpirationConfiguration(
|
||||
threadId,
|
||||
message.expiryMode,
|
||||
message.sentTimestamp!!
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e("Loki", "Failed to update expiration configuration.")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,6 +182,7 @@ private fun MessageReceiver.handleDataExtractionNotification(message: DataExtrac
|
||||
if (message.groupPublicKey != null) return
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val senderPublicKey = message.sender!!
|
||||
|
||||
val notification: DataExtractionNotificationInfoMessage = when(message.kind) {
|
||||
is DataExtractionNotification.Kind.Screenshot -> DataExtractionNotificationInfoMessage(DataExtractionNotificationInfoMessage.Kind.SCREENSHOT)
|
||||
is DataExtractionNotification.Kind.MediaSaved -> DataExtractionNotificationInfoMessage(DataExtractionNotificationInfoMessage.Kind.MEDIA_SAVED)
|
||||
@@ -148,15 +203,21 @@ private fun handleConfigurationMessage(message: ConfigurationMessage) {
|
||||
|
||||
TextSecurePreferences.setConfigurationMessageSynced(context, true)
|
||||
TextSecurePreferences.setLastProfileUpdateTime(context, message.sentTimestamp!!)
|
||||
val isForceSync = TextSecurePreferences.hasForcedNewConfig(context)
|
||||
val currentTime = SnodeAPI.nowWithOffset
|
||||
if (ConfigBase.isNewConfigEnabled(isForceSync, currentTime)) {
|
||||
TextSecurePreferences.setHasLegacyConfig(context, true)
|
||||
if (!firstTimeSync) return
|
||||
}
|
||||
val allClosedGroupPublicKeys = storage.getAllClosedGroupPublicKeys()
|
||||
for (closedGroup in message.closedGroups) {
|
||||
if (allClosedGroupPublicKeys.contains(closedGroup.publicKey)) {
|
||||
// just handle the closed group encryption key pairs to avoid sync'd devices getting out of sync
|
||||
storage.addClosedGroupEncryptionKeyPair(closedGroup.encryptionKeyPair!!, closedGroup.publicKey)
|
||||
} else if (firstTimeSync) {
|
||||
storage.addClosedGroupEncryptionKeyPair(closedGroup.encryptionKeyPair!!, closedGroup.publicKey, message.sentTimestamp!!)
|
||||
} else {
|
||||
// only handle new closed group if it's first time sync
|
||||
handleNewClosedGroup(message.sender!!, message.sentTimestamp!!, closedGroup.publicKey, closedGroup.name,
|
||||
closedGroup.encryptionKeyPair!!, closedGroup.members, closedGroup.admins, message.sentTimestamp!!, closedGroup.expirationTimer)
|
||||
closedGroup.encryptionKeyPair!!, closedGroup.members, closedGroup.admins, message.sentTimestamp!!, -1)
|
||||
}
|
||||
}
|
||||
val allV2OpenGroups = storage.getAllOpenGroups().map { it.value.joinURL }
|
||||
@@ -165,9 +226,9 @@ private fun handleConfigurationMessage(message: ConfigurationMessage) {
|
||||
.replace(OpenGroupApi.httpDefaultServer, OpenGroupApi.defaultServer)
|
||||
}) {
|
||||
if (allV2OpenGroups.contains(openGroup)) continue
|
||||
Log.d("OpenGroup", "All open groups doesn't contain $openGroup")
|
||||
Log.d("OpenGroup", "All open groups doesn't contain open group")
|
||||
if (!storage.hasBackgroundGroupAddJob(openGroup)) {
|
||||
Log.d("OpenGroup", "Doesn't contain background job for $openGroup, adding")
|
||||
Log.d("OpenGroup", "Doesn't contain background job for open group, adding")
|
||||
JobQueue.shared.add(BackgroundGroupAddJob(openGroup))
|
||||
}
|
||||
}
|
||||
@@ -181,30 +242,29 @@ private fun handleConfigurationMessage(message: ConfigurationMessage) {
|
||||
&& TextSecurePreferences.getProfilePictureURL(context) != message.profilePicture) {
|
||||
val profileKey = Base64.encodeBytes(message.profileKey)
|
||||
ProfileKeyUtil.setEncodedProfileKey(context, profileKey)
|
||||
profileManager.setProfileKey(context, recipient, message.profileKey)
|
||||
if (!message.profilePicture.isNullOrEmpty() && TextSecurePreferences.getProfilePictureURL(context) != message.profilePicture) {
|
||||
storage.setUserProfilePictureURL(message.profilePicture!!)
|
||||
}
|
||||
profileManager.setProfilePicture(context, recipient, message.profilePicture, message.profileKey)
|
||||
}
|
||||
storage.addContacts(message.contacts)
|
||||
}
|
||||
|
||||
fun MessageReceiver.handleUnsendRequest(message: UnsendRequest) {
|
||||
fun MessageReceiver.handleUnsendRequest(message: UnsendRequest): Long? {
|
||||
val userPublicKey = MessagingModuleConfiguration.shared.storage.getUserPublicKey()
|
||||
if (message.sender != message.author && (message.sender != userPublicKey && userPublicKey != null)) { return }
|
||||
if (message.sender != message.author && (message.sender != userPublicKey && userPublicKey != null)) { return null }
|
||||
val context = MessagingModuleConfiguration.shared.context
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
|
||||
val timestamp = message.timestamp ?: return
|
||||
val author = message.author ?: return
|
||||
val messageIdToDelete = storage.getMessageIdInDatabase(timestamp, author) ?: return
|
||||
messageDataProvider.getServerHashForMessage(messageIdToDelete)?.let { serverHash ->
|
||||
val timestamp = message.timestamp ?: return null
|
||||
val author = message.author ?: return null
|
||||
val (messageIdToDelete, mms) = storage.getMessageIdInDatabase(timestamp, author) ?: return null
|
||||
messageDataProvider.getServerHashForMessage(messageIdToDelete, mms)?.let { serverHash ->
|
||||
SnodeAPI.deleteMessage(author, listOf(serverHash))
|
||||
}
|
||||
messageDataProvider.updateMessageAsDeleted(timestamp, author)
|
||||
if (!messageDataProvider.isOutgoingMessage(messageIdToDelete)) {
|
||||
val deletedMessageId = messageDataProvider.updateMessageAsDeleted(timestamp, author)
|
||||
if (!messageDataProvider.isOutgoingMessage(timestamp)) {
|
||||
SSKEnvironment.shared.notificationManager.updateNotification(context)
|
||||
}
|
||||
|
||||
return deletedMessageId
|
||||
}
|
||||
|
||||
fun handleMessageRequestResponse(message: MessageRequestResponse) {
|
||||
@@ -212,24 +272,37 @@ fun handleMessageRequestResponse(message: MessageRequestResponse) {
|
||||
}
|
||||
//endregion
|
||||
|
||||
fun MessageReceiver.handleVisibleMessage(message: VisibleMessage,
|
||||
proto: SignalServiceProtos.Content,
|
||||
openGroupID: String?,
|
||||
runIncrement: Boolean,
|
||||
runThreadUpdate: Boolean,
|
||||
runProfileUpdate: Boolean): Long? {
|
||||
private fun SignalServiceProtos.Content.ExpirationType.expiryMode(durationSeconds: Long) = takeIf { durationSeconds > 0 }?.let {
|
||||
when (it) {
|
||||
SignalServiceProtos.Content.ExpirationType.DELETE_AFTER_READ -> ExpiryMode.AfterRead(durationSeconds)
|
||||
SignalServiceProtos.Content.ExpirationType.DELETE_AFTER_SEND, SignalServiceProtos.Content.ExpirationType.UNKNOWN -> ExpiryMode.AfterSend(durationSeconds)
|
||||
else -> ExpiryMode.NONE
|
||||
}
|
||||
} ?: ExpiryMode.NONE
|
||||
|
||||
fun MessageReceiver.handleVisibleMessage(
|
||||
message: VisibleMessage,
|
||||
proto: SignalServiceProtos.Content,
|
||||
openGroupID: String?,
|
||||
threadId: Long,
|
||||
runThreadUpdate: Boolean,
|
||||
runProfileUpdate: Boolean
|
||||
): Long? {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val context = MessagingModuleConfiguration.shared.context
|
||||
message.takeIf { it.isSenderSelf }?.sentTimestamp?.let { MessagingModuleConfiguration.shared.lastSentTimestampCache.submitTimestamp(threadId, it) }
|
||||
val userPublicKey = storage.getUserPublicKey()
|
||||
val messageSender: String? = message.sender
|
||||
|
||||
// Do nothing if the message was outdated
|
||||
if (MessageReceiver.messageIsOutdated(message, threadId, openGroupID)) { return null }
|
||||
|
||||
// Get or create thread
|
||||
// FIXME: In case this is an open group this actually * doesn't * create the thread if it doesn't yet
|
||||
// exist. This is intentional, but it's very non-obvious.
|
||||
val threadID = storage.getOrCreateThreadIdFor(message.syncTarget ?: messageSender!!, message.groupPublicKey, openGroupID)
|
||||
if (threadID < 0) {
|
||||
val threadID = storage.getThreadIdFor(message.syncTarget ?: messageSender!!, message.groupPublicKey, openGroupID, createThread = true)
|
||||
// Thread doesn't exist; should only be reached in a case where we are processing open group messages for a no longer existent thread
|
||||
throw MessageReceiver.Error.NoThread
|
||||
}
|
||||
?: throw MessageReceiver.Error.NoThread
|
||||
val threadRecipient = storage.getRecipientForThread(threadID)
|
||||
val userBlindedKey = openGroupID?.let {
|
||||
val openGroup = storage.getOpenGroup(threadID) ?: return@let null
|
||||
@@ -256,14 +329,31 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage,
|
||||
val profileKeyChanged = (recipient.profileKey == null || !MessageDigest.isEqual(recipient.profileKey, newProfileKey))
|
||||
|
||||
if ((profileKeyValid && profileKeyChanged) || (profileKeyValid && needsProfilePicture)) {
|
||||
profileManager.setProfileKey(context, recipient, newProfileKey!!)
|
||||
profileManager.setProfilePicture(context, recipient, profile.profilePictureURL, newProfileKey)
|
||||
profileManager.setUnidentifiedAccessMode(context, recipient, Recipient.UnidentifiedAccessMode.UNKNOWN)
|
||||
profileManager.setProfilePictureURL(context, recipient, profile.profilePictureURL!!)
|
||||
} else if (newProfileKey == null || newProfileKey.isEmpty() || profile.profilePictureURL.isNullOrEmpty()) {
|
||||
profileManager.setProfilePicture(context, recipient, null, null)
|
||||
}
|
||||
}
|
||||
|
||||
if (userPublicKey != messageSender && !isUserBlindedSender) {
|
||||
storage.setBlocksCommunityMessageRequests(recipient, message.blocksMessageRequests)
|
||||
}
|
||||
|
||||
// update the disappearing / legacy banner for the sender
|
||||
val disappearingState = when {
|
||||
proto.dataMessage.expireTimer > 0 && !proto.hasExpirationType() -> Recipient.DisappearingState.LEGACY
|
||||
else -> Recipient.DisappearingState.UPDATED
|
||||
}
|
||||
storage.updateDisappearingState(
|
||||
messageSender,
|
||||
threadID,
|
||||
disappearingState
|
||||
)
|
||||
}
|
||||
// Parse quote if needed
|
||||
var quoteModel: QuoteModel? = null
|
||||
var quoteMessageBody: String? = null
|
||||
if (message.quote != null && proto.dataMessage.hasQuote()) {
|
||||
val quote = proto.dataMessage.quote
|
||||
|
||||
@@ -275,6 +365,7 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage,
|
||||
|
||||
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
|
||||
val messageInfo = messageDataProvider.getMessageForQuote(quote.id, author)
|
||||
quoteMessageBody = messageInfo?.third
|
||||
quoteModel = if (messageInfo != null) {
|
||||
val attachments = if (messageInfo.second) messageDataProvider.getAttachmentsAndLinkPreviewFor(messageInfo.first) else ArrayList()
|
||||
QuoteModel(quote.id, author,null,false, attachments)
|
||||
@@ -299,14 +390,9 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage,
|
||||
}
|
||||
}
|
||||
// Parse attachments if needed
|
||||
val attachments = proto.dataMessage.attachmentsList.mapNotNull { attachmentProto ->
|
||||
val attachment = Attachment.fromProto(attachmentProto)
|
||||
if (!attachment.isValid()) {
|
||||
return@mapNotNull null
|
||||
} else {
|
||||
return@mapNotNull attachment
|
||||
}
|
||||
}
|
||||
val attachments = proto.dataMessage.attachmentsList.map(Attachment::fromProto).filter { it.isValid() }
|
||||
// Cancel any typing indicators if needed
|
||||
cancelTypingIndicatorsIfNeeded(message.sender!!)
|
||||
// Parse reaction if needed
|
||||
val threadIsGroup = threadRecipient?.isGroupRecipient == true
|
||||
message.reaction?.let { reaction ->
|
||||
@@ -319,21 +405,25 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage,
|
||||
storage.removeReaction(reaction.emoji!!, reaction.timestamp!!, reaction.publicKey!!, threadIsGroup)
|
||||
}
|
||||
} ?: run {
|
||||
// A user is mentioned if their public key is in the body of a message or one of their messages
|
||||
// was quoted
|
||||
val messageText = message.text
|
||||
message.hasMention = listOf(userPublicKey, userBlindedKey)
|
||||
.filterNotNull()
|
||||
.any { key ->
|
||||
messageText?.contains("@$key") == true || key == (quoteModel?.author?.serialize() ?: "")
|
||||
}
|
||||
|
||||
// Persist the message
|
||||
message.threadID = threadID
|
||||
val messageID =
|
||||
storage.persist(message, quoteModel, linkPreviews, message.groupPublicKey, openGroupID,
|
||||
attachments, runIncrement, runThreadUpdate
|
||||
) ?: return null
|
||||
val openGroupServerID = message.openGroupServerMessageID
|
||||
if (openGroupServerID != null) {
|
||||
val isSms = !(message.isMediaMessage() || attachments.isNotEmpty())
|
||||
storage.setOpenGroupServerMessageID(messageID, openGroupServerID, threadID, isSms)
|
||||
val messageID = storage.persist(message, quoteModel, linkPreviews, message.groupPublicKey, openGroupID, attachments, runThreadUpdate) ?: return null
|
||||
message.openGroupServerMessageID?.let {
|
||||
val isSms = !message.isMediaMessage() && attachments.isEmpty()
|
||||
storage.setOpenGroupServerMessageID(messageID, it, threadID, isSms)
|
||||
}
|
||||
SSKEnvironment.shared.messageExpirationManager.maybeStartExpiration(message)
|
||||
return messageID
|
||||
}
|
||||
// Cancel any typing indicators if needed
|
||||
cancelTypingIndicatorsIfNeeded(message.sender!!)
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -418,26 +508,65 @@ private fun MessageReceiver.handleClosedGroupControlMessage(message: ClosedGroup
|
||||
is ClosedGroupControlMessage.Kind.MembersRemoved -> handleClosedGroupMembersRemoved(message)
|
||||
is ClosedGroupControlMessage.Kind.MemberLeft -> handleClosedGroupMemberLeft(message)
|
||||
}
|
||||
if (
|
||||
message.kind !is ClosedGroupControlMessage.Kind.New &&
|
||||
MessagingModuleConfiguration.shared.storage.canPerformConfigChange(
|
||||
SharedConfigMessage.Kind.GROUPS.name,
|
||||
MessagingModuleConfiguration.shared.storage.getUserPublicKey()!!,
|
||||
message.sentTimestamp!!
|
||||
)
|
||||
) {
|
||||
// update the config
|
||||
val closedGroupPublicKey = message.getPublicKey()
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
storage.updateGroupConfig(closedGroupPublicKey)
|
||||
}
|
||||
}
|
||||
|
||||
private fun ClosedGroupControlMessage.getPublicKey(): String = kind!!.let { when (it) {
|
||||
is ClosedGroupControlMessage.Kind.New -> it.publicKey.toByteArray().toHexString()
|
||||
is ClosedGroupControlMessage.Kind.EncryptionKeyPair -> it.publicKey?.toByteArray()?.toHexString() ?: groupPublicKey!!
|
||||
is ClosedGroupControlMessage.Kind.MemberLeft -> groupPublicKey!!
|
||||
is ClosedGroupControlMessage.Kind.MembersAdded -> groupPublicKey!!
|
||||
is ClosedGroupControlMessage.Kind.MembersRemoved -> groupPublicKey!!
|
||||
is ClosedGroupControlMessage.Kind.NameChange -> groupPublicKey!!
|
||||
}}
|
||||
|
||||
private fun MessageReceiver.handleNewClosedGroup(message: ClosedGroupControlMessage) {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val kind = message.kind!! as? ClosedGroupControlMessage.Kind.New ?: return
|
||||
val recipient = Recipient.from(MessagingModuleConfiguration.shared.context, Address.fromSerialized(message.sender!!), false)
|
||||
if (!recipient.isApproved) return
|
||||
if (!recipient.isApproved && !recipient.isLocalNumber) return Log.e("Loki", "not accepting new closed group from unapproved recipient")
|
||||
val groupPublicKey = kind.publicKey.toByteArray().toHexString()
|
||||
// hard code check by group public key in the big function because I can't be bothered to do group double decode re-encodej
|
||||
if ((storage.getThreadIdFor(message.sender!!, groupPublicKey, null, false) ?: -1L) >= 0L) return
|
||||
val members = kind.members.map { it.toByteArray().toHexString() }
|
||||
val admins = kind.admins.map { it.toByteArray().toHexString() }
|
||||
val expireTimer = kind.expirationTimer
|
||||
handleNewClosedGroup(message.sender!!, message.sentTimestamp!!, groupPublicKey, kind.name, kind.encryptionKeyPair!!, members, admins, message.sentTimestamp!!, expireTimer)
|
||||
val expirationTimer = kind.expirationTimer
|
||||
handleNewClosedGroup(message.sender!!, message.sentTimestamp!!, groupPublicKey, kind.name, kind.encryptionKeyPair!!, members, admins, message.sentTimestamp!!, expirationTimer)
|
||||
}
|
||||
|
||||
private fun handleNewClosedGroup(sender: String, sentTimestamp: Long, groupPublicKey: String, name: String, encryptionKeyPair: ECKeyPair, members: List<String>, admins: List<String>, formationTimestamp: Long, expireTimer: Int) {
|
||||
private fun handleNewClosedGroup(sender: String, sentTimestamp: Long, groupPublicKey: String, name: String, encryptionKeyPair: ECKeyPair, members: List<String>, admins: List<String>, formationTimestamp: Long, expirationTimer: Int) {
|
||||
val context = MessagingModuleConfiguration.shared.context
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
|
||||
// Create the group
|
||||
val userPublicKey = storage.getUserPublicKey()!!
|
||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||
val groupExists = storage.getGroup(groupID) != null
|
||||
|
||||
if (!storage.canPerformConfigChange(SharedConfigMessage.Kind.GROUPS.name, userPublicKey, sentTimestamp)) {
|
||||
// If the closed group already exists then store the encryption keys (since the config only stores
|
||||
// the latest key we won't be able to decrypt older messages if we were added to the group within
|
||||
// the last two weeks and the key has been rotated - unfortunately if the user was added more than
|
||||
// two weeks ago and the keys were rotated within the last two weeks then we won't be able to decrypt
|
||||
// messages received before the key rotation)
|
||||
if (groupExists) {
|
||||
storage.addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey, sentTimestamp)
|
||||
storage.updateGroupConfig(groupPublicKey)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Create the group
|
||||
if (groupExists) {
|
||||
// Update the group
|
||||
if (!storage.isGroupActive(groupPublicKey)) {
|
||||
@@ -449,18 +578,17 @@ private fun handleNewClosedGroup(sender: String, sentTimestamp: Long, groupPubli
|
||||
storage.updateTitle(groupID, name)
|
||||
storage.updateMembers(groupID, members.map { Address.fromSerialized(it) })
|
||||
} else {
|
||||
storage.createGroup(groupID, name, LinkedList(members.map { Address.fromSerialized(it) }),
|
||||
storage.createGroup(groupID, name, LinkedList(members.map { fromSerialized(it) }),
|
||||
null, null, LinkedList(admins.map { Address.fromSerialized(it) }), formationTimestamp)
|
||||
}
|
||||
storage.setProfileSharing(Address.fromSerialized(groupID), true)
|
||||
// Add the group to the user's set of public keys to poll for
|
||||
storage.addClosedGroupPublicKey(groupPublicKey)
|
||||
// Store the encryption key pair
|
||||
storage.addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey)
|
||||
// Set expiration timer
|
||||
storage.setExpirationTimer(groupID, expireTimer)
|
||||
storage.addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey, sentTimestamp)
|
||||
storage.createInitialConfigGroup(groupPublicKey, name, GroupUtil.createConfigMemberMap(members, admins), formationTimestamp, encryptionKeyPair, expirationTimer)
|
||||
// Notify the PN server
|
||||
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Subscribe, groupPublicKey, storage.getUserPublicKey()!!)
|
||||
PushRegistryV1.register(device = MessagingModuleConfiguration.shared.device, publicKey = userPublicKey)
|
||||
// Notify the user
|
||||
if (userPublicKey == sender && !groupExists) {
|
||||
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||
@@ -508,7 +636,7 @@ private fun MessageReceiver.handleClosedGroupEncryptionKeyPair(message: ClosedGr
|
||||
Log.d("Loki", "Ignoring duplicate closed group encryption key pair.")
|
||||
return
|
||||
}
|
||||
storage.addClosedGroupEncryptionKeyPair(keyPair, groupPublicKey)
|
||||
storage.addClosedGroupEncryptionKeyPair(keyPair, groupPublicKey, message.sentTimestamp!!)
|
||||
Log.d("Loki", "Received a new closed group encryption key pair.")
|
||||
}
|
||||
|
||||
@@ -536,7 +664,12 @@ private fun MessageReceiver.handleClosedGroupNameChanged(message: ClosedGroupCon
|
||||
val members = group.members.map { it.serialize() }
|
||||
val admins = group.admins.map { it.serialize() }
|
||||
val name = kind.name
|
||||
storage.updateTitle(groupID, name)
|
||||
|
||||
// Only update the group in storage if it isn't invalidated by the config state
|
||||
if (storage.canPerformConfigChange(SharedConfigMessage.Kind.GROUPS.name, userPublicKey!!, message.sentTimestamp!!)) {
|
||||
storage.updateTitle(groupID, name)
|
||||
}
|
||||
|
||||
// Notify the user
|
||||
if (userPublicKey == senderPublicKey) {
|
||||
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||
@@ -570,12 +703,16 @@ private fun MessageReceiver.handleClosedGroupMembersAdded(message: ClosedGroupCo
|
||||
|
||||
val updateMembers = kind.members.map { it.toByteArray().toHexString() }
|
||||
val newMembers = members + updateMembers
|
||||
storage.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) })
|
||||
|
||||
// Update zombie members in case the added members are zombies
|
||||
val zombies = storage.getZombieMembers(groupID)
|
||||
if (zombies.intersect(updateMembers).isNotEmpty()) {
|
||||
storage.setZombieMembers(groupID, zombies.minus(updateMembers).map { Address.fromSerialized(it) })
|
||||
// Only update the group in storage if it isn't invalidated by the config state
|
||||
if (storage.canPerformConfigChange(SharedConfigMessage.Kind.GROUPS.name, userPublicKey, message.sentTimestamp!!)) {
|
||||
storage.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) })
|
||||
|
||||
// Update zombie members in case the added members are zombies
|
||||
val zombies = storage.getZombieMembers(groupID)
|
||||
if (zombies.intersect(updateMembers).isNotEmpty()) {
|
||||
storage.setZombieMembers(groupID, zombies.minus(updateMembers).map { Address.fromSerialized(it) })
|
||||
}
|
||||
}
|
||||
|
||||
// Notify the user
|
||||
@@ -657,13 +794,18 @@ private fun MessageReceiver.handleClosedGroupMembersRemoved(message: ClosedGroup
|
||||
Log.d("Loki", "Received a MEMBERS_REMOVED instead of a MEMBERS_LEFT from sender: $senderPublicKey.")
|
||||
}
|
||||
val wasCurrentUserRemoved = userPublicKey in removedMembers
|
||||
// Admin should send a MEMBERS_LEFT message but handled here just in case
|
||||
if (didAdminLeave || wasCurrentUserRemoved) {
|
||||
disableLocalGroupAndUnsubscribe(groupPublicKey, groupID, userPublicKey)
|
||||
} else {
|
||||
storage.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) })
|
||||
// Update zombie members
|
||||
storage.setZombieMembers(groupID, zombies.minus(removedMembers).map { Address.fromSerialized(it) })
|
||||
|
||||
// Only update the group in storage if it isn't invalidated by the config state
|
||||
if (storage.canPerformConfigChange(SharedConfigMessage.Kind.GROUPS.name, userPublicKey, message.sentTimestamp!!)) {
|
||||
// Admin should send a MEMBERS_LEFT message but handled here just in case
|
||||
if (didAdminLeave || wasCurrentUserRemoved) {
|
||||
disableLocalGroupAndUnsubscribe(groupPublicKey, groupID, userPublicKey, true)
|
||||
return
|
||||
} else {
|
||||
storage.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) })
|
||||
// Update zombie members
|
||||
storage.setZombieMembers(groupID, zombies.minus(removedMembers).map { Address.fromSerialized(it) })
|
||||
}
|
||||
}
|
||||
|
||||
// Notify the user
|
||||
@@ -712,24 +854,30 @@ private fun MessageReceiver.handleClosedGroupMemberLeft(message: ClosedGroupCont
|
||||
val didAdminLeave = admins.contains(senderPublicKey)
|
||||
val updatedMemberList = members - senderPublicKey
|
||||
val userLeft = (userPublicKey == senderPublicKey)
|
||||
if (didAdminLeave || userLeft) {
|
||||
disableLocalGroupAndUnsubscribe(groupPublicKey, groupID, userPublicKey)
|
||||
} else {
|
||||
storage.updateMembers(groupID, updatedMemberList.map { Address.fromSerialized(it) })
|
||||
// Update zombie members
|
||||
val zombies = storage.getZombieMembers(groupID)
|
||||
storage.setZombieMembers(groupID, zombies.plus(senderPublicKey).map { Address.fromSerialized(it) })
|
||||
|
||||
// Only update the group in storage if it isn't invalidated by the config state
|
||||
if (storage.canPerformConfigChange(SharedConfigMessage.Kind.GROUPS.name, userPublicKey, message.sentTimestamp!!)) {
|
||||
if (didAdminLeave || userLeft) {
|
||||
disableLocalGroupAndUnsubscribe(groupPublicKey, groupID, userPublicKey, delete = userLeft)
|
||||
|
||||
if (userLeft) {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
storage.updateMembers(groupID, updatedMemberList.map { Address.fromSerialized(it) })
|
||||
// Update zombie members
|
||||
val zombies = storage.getZombieMembers(groupID)
|
||||
storage.setZombieMembers(groupID, zombies.plus(senderPublicKey).map { Address.fromSerialized(it) })
|
||||
}
|
||||
}
|
||||
|
||||
// Notify the user
|
||||
if (userLeft) {
|
||||
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||
storage.insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.QUIT, name, members, admins, threadID, message.sentTimestamp!!)
|
||||
} else {
|
||||
if (!userLeft) {
|
||||
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceGroup.Type.QUIT, name, members, admins, message.sentTimestamp!!)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isValidGroupUpdate(group: GroupRecord, sentTimestamp: Long, senderPublicKey: String): Boolean {
|
||||
private fun isValidGroupUpdate(group: GroupRecord, sentTimestamp: Long, senderPublicKey: String): Boolean {
|
||||
val oldMembers = group.members.map { it.serialize() }
|
||||
// Check that the message isn't from before the group was created
|
||||
if (group.formationTimestamp > sentTimestamp) {
|
||||
@@ -744,7 +892,7 @@ private fun isValidGroupUpdate(group: GroupRecord, sentTimestamp: Long, senderPu
|
||||
return true
|
||||
}
|
||||
|
||||
fun MessageReceiver.disableLocalGroupAndUnsubscribe(groupPublicKey: String, groupID: String, userPublicKey: String) {
|
||||
fun MessageReceiver.disableLocalGroupAndUnsubscribe(groupPublicKey: String, groupID: String, userPublicKey: String, delete: Boolean) {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
storage.removeClosedGroupPublicKey(groupPublicKey)
|
||||
// Remove the key pairs
|
||||
@@ -753,8 +901,14 @@ fun MessageReceiver.disableLocalGroupAndUnsubscribe(groupPublicKey: String, grou
|
||||
storage.setActive(groupID, false)
|
||||
storage.removeMember(groupID, Address.fromSerialized(userPublicKey))
|
||||
// Notify the PN server
|
||||
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Unsubscribe, groupPublicKey, userPublicKey)
|
||||
PushRegistryV1.unsubscribeGroup(groupPublicKey, publicKey = userPublicKey)
|
||||
// Stop polling
|
||||
ClosedGroupPollerV2.shared.stopPolling(groupPublicKey)
|
||||
|
||||
if (delete) {
|
||||
val threadId = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||
storage.cancelPendingMessageSendJobs(threadId)
|
||||
storage.deleteConversation(threadId)
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
@@ -13,6 +13,7 @@ import org.session.libsignal.utilities.JsonUtil;
|
||||
import org.session.libsignal.utilities.guava.Optional;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
|
||||
public class LinkPreview {
|
||||
|
||||
@@ -75,4 +76,17 @@ public class LinkPreview {
|
||||
public static LinkPreview deserialize(@NonNull String serialized) throws IOException {
|
||||
return JsonUtil.fromJson(serialized, LinkPreview.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
LinkPreview that = (LinkPreview) o;
|
||||
return Objects.equals(url, that.url) && Objects.equals(title, that.title) && Objects.equals(attachmentId, that.attachmentId) && Objects.equals(thumbnail, that.thumbnail);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(url, title, attachmentId, thumbnail);
|
||||
}
|
||||
}
|
||||
|
@@ -14,4 +14,4 @@ interface MessageNotifier {
|
||||
fun updateNotification(context: Context, threadId: Long, signal: Boolean)
|
||||
fun updateNotification(context: Context, signal: Boolean, reminderCount: Int)
|
||||
fun clearReminder(context: Context)
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,126 @@
|
||||
package org.session.libsession.messaging.sending_receiving.notifications
|
||||
|
||||
import com.goterl.lazysodium.utils.Key
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* N.B. all of these variable names will be named the same as the actual JSON utf-8 request/responses expected from the server.
|
||||
* Changing the variable names will break how data is serialized/deserialized.
|
||||
* If it's less than ideally named we can use [SerialName], such as for the push metadata which uses
|
||||
* single-letter keys to be as compact as possible.
|
||||
*/
|
||||
|
||||
@Serializable
|
||||
data class SubscriptionRequest(
|
||||
/** the 33-byte account being subscribed to; typically a session ID */
|
||||
val pubkey: String,
|
||||
/** when the pubkey starts with 05 (i.e. a session ID) this is the ed25519 32-byte pubkey associated with the session ID */
|
||||
val session_ed25519: String?,
|
||||
/** 32-byte swarm authentication subkey; omitted (or null) when not using subkey auth (new closed groups) */
|
||||
val subkey_tag: String? = null,
|
||||
/** array of integer namespaces to subscribe to, **must be sorted in ascending order** */
|
||||
val namespaces: List<Int>,
|
||||
/** if provided and true then notifications will include the body of the message (as long as it isn't too large) */
|
||||
val data: Boolean,
|
||||
/** the signature unix timestamp in seconds, not ms */
|
||||
val sig_ts: Long,
|
||||
/** the 64-byte ed25519 signature */
|
||||
val signature: String,
|
||||
/** the string identifying the notification service, "firebase" for android (currently) */
|
||||
val service: String,
|
||||
/** dict of service-specific data, currently just "token" field with device-specific token but different services might have other requirements */
|
||||
val service_info: Map<String, String>,
|
||||
/** 32-byte encryption key; notification payloads sent to the device will be encrypted with XChaCha20-Poly1305 via libsodium using this key.
|
||||
* persist it on device */
|
||||
val enc_key: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class UnsubscriptionRequest(
|
||||
/** the 33-byte account being subscribed to; typically a session ID */
|
||||
val pubkey: String,
|
||||
/** when the pubkey starts with 05 (i.e. a session ID) this is the ed25519 32-byte pubkey associated with the session ID */
|
||||
val session_ed25519: String?,
|
||||
/** 32-byte swarm authentication subkey; omitted (or null) when not using subkey auth (new closed groups) */
|
||||
val subkey_tag: String? = null,
|
||||
/** the signature unix timestamp in seconds, not ms */
|
||||
val sig_ts: Long,
|
||||
/** the 64-byte ed25519 signature */
|
||||
val signature: String,
|
||||
/** the string identifying the notification service, "firebase" for android (currently) */
|
||||
val service: String,
|
||||
/** dict of service-specific data, currently just "token" field with device-specific token but different services might have other requirements */
|
||||
val service_info: Map<String, String>,
|
||||
)
|
||||
|
||||
/** invalid values, missing reuqired arguments etc, details in message */
|
||||
private const val UNPARSEABLE_ERROR = 1
|
||||
/** the "service" value is not active / valid */
|
||||
private const val SERVICE_NOT_AVAILABLE = 2
|
||||
/** something getting wrong internally talking to the backend */
|
||||
private const val SERVICE_TIMEOUT = 3
|
||||
/** other error processing the subscription (details in the message) */
|
||||
private const val GENERIC_ERROR = 4
|
||||
|
||||
@Serializable
|
||||
data class SubscriptionResponse(
|
||||
override val error: Int? = null,
|
||||
override val message: String? = null,
|
||||
override val success: Boolean? = null,
|
||||
val added: Boolean? = null,
|
||||
val updated: Boolean? = null,
|
||||
): Response
|
||||
|
||||
@Serializable
|
||||
data class UnsubscribeResponse(
|
||||
override val error: Int? = null,
|
||||
override val message: String? = null,
|
||||
override val success: Boolean? = null,
|
||||
val removed: Boolean? = null,
|
||||
): Response
|
||||
|
||||
interface Response {
|
||||
val error: Int?
|
||||
val message: String?
|
||||
val success: Boolean?
|
||||
fun isSuccess() = success == true && error == null
|
||||
fun isFailure() = !isSuccess()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class PushNotificationMetadata(
|
||||
/** Account ID (such as Session ID or closed group ID) where the message arrived **/
|
||||
@SerialName("@")
|
||||
val account: String,
|
||||
|
||||
/** The hash of the message in the swarm. */
|
||||
@SerialName("#")
|
||||
val msg_hash: String,
|
||||
|
||||
/** The swarm namespace in which this message arrived. */
|
||||
@SerialName("n")
|
||||
val namespace: Int,
|
||||
|
||||
/** The length of the message data. This is always included, even if the message content
|
||||
* itself was too large to fit into the push notification. */
|
||||
@SerialName("l")
|
||||
val data_len: Int,
|
||||
|
||||
/** This will be true if the data was omitted because it was too long to fit in a push
|
||||
* notification (around 2.5kB of raw data), in which case the push notification includes
|
||||
* only this metadata but not the message content itself. */
|
||||
@SerialName("B")
|
||||
val data_too_long : Boolean = false
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PushNotificationServerObject(
|
||||
val enc_payload: String,
|
||||
val spns: Int,
|
||||
) {
|
||||
fun decryptPayload(key: Key): Any {
|
||||
|
||||
TODO()
|
||||
}
|
||||
}
|
@@ -1,107 +0,0 @@
|
||||
package org.session.libsession.messaging.sending_receiving.notifications
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import nl.komponents.kovenant.functional.map
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.snode.OnionRequestAPI
|
||||
import org.session.libsession.snode.Version
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.utilities.retryIfNeeded
|
||||
import org.session.libsignal.utilities.JsonUtil
|
||||
import org.session.libsignal.utilities.Log
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
object PushNotificationAPI {
|
||||
val context = MessagingModuleConfiguration.shared.context
|
||||
val server = "https://live.apns.getsession.org"
|
||||
val serverPublicKey = "642a6585919742e5a2d4dc51244964fbcd8bcab2b75612407de58b810740d049"
|
||||
private val maxRetryCount = 4
|
||||
private val tokenExpirationInterval = 12 * 60 * 60 * 1000
|
||||
|
||||
enum class ClosedGroupOperation {
|
||||
Subscribe, Unsubscribe;
|
||||
|
||||
val rawValue: String
|
||||
get() {
|
||||
return when (this) {
|
||||
Subscribe -> "subscribe_closed_group"
|
||||
Unsubscribe -> "unsubscribe_closed_group"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun unregister(token: String) {
|
||||
val parameters = mapOf( "token" to token )
|
||||
val url = "$server/unregister"
|
||||
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
|
||||
val request = Request.Builder().url(url).post(body)
|
||||
retryIfNeeded(maxRetryCount) {
|
||||
OnionRequestAPI.sendOnionRequest(request.build(), server, serverPublicKey, Version.V2).map { response ->
|
||||
val code = response.info["code"] as? Int
|
||||
if (code != null && code != 0) {
|
||||
TextSecurePreferences.setIsUsingFCM(context, false)
|
||||
} else {
|
||||
Log.d("Loki", "Couldn't disable FCM due to error: ${response.info["message"] as? String ?: "null"}.")
|
||||
}
|
||||
}.fail { exception ->
|
||||
Log.d("Loki", "Couldn't disable FCM due to error: ${exception}.")
|
||||
}
|
||||
}
|
||||
// Unsubscribe from all closed groups
|
||||
val allClosedGroupPublicKeys = MessagingModuleConfiguration.shared.storage.getAllClosedGroupPublicKeys()
|
||||
val userPublicKey = MessagingModuleConfiguration.shared.storage.getUserPublicKey()!!
|
||||
allClosedGroupPublicKeys.iterator().forEach { closedGroup ->
|
||||
performOperation(ClosedGroupOperation.Unsubscribe, closedGroup, userPublicKey)
|
||||
}
|
||||
}
|
||||
|
||||
fun register(token: String, publicKey: String, force: Boolean) {
|
||||
val oldToken = TextSecurePreferences.getFCMToken(context)
|
||||
val lastUploadDate = TextSecurePreferences.getLastFCMUploadTime(context)
|
||||
if (!force && token == oldToken && System.currentTimeMillis() - lastUploadDate < tokenExpirationInterval) { return }
|
||||
val parameters = mapOf( "token" to token, "pubKey" to publicKey )
|
||||
val url = "$server/register"
|
||||
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
|
||||
val request = Request.Builder().url(url).post(body)
|
||||
retryIfNeeded(maxRetryCount) {
|
||||
OnionRequestAPI.sendOnionRequest(request.build(), server, serverPublicKey, Version.V2).map { response ->
|
||||
val code = response.info["code"] as? Int
|
||||
if (code != null && code != 0) {
|
||||
TextSecurePreferences.setIsUsingFCM(context, true)
|
||||
TextSecurePreferences.setFCMToken(context, token)
|
||||
TextSecurePreferences.setLastFCMUploadTime(context, System.currentTimeMillis())
|
||||
} else {
|
||||
Log.d("Loki", "Couldn't register for FCM due to error: ${response.info["message"] as? String ?: "null"}.")
|
||||
}
|
||||
}.fail { exception ->
|
||||
Log.d("Loki", "Couldn't register for FCM due to error: ${exception}.")
|
||||
}
|
||||
}
|
||||
// Subscribe to all closed groups
|
||||
val allClosedGroupPublicKeys = MessagingModuleConfiguration.shared.storage.getAllClosedGroupPublicKeys()
|
||||
allClosedGroupPublicKeys.iterator().forEach { closedGroup ->
|
||||
performOperation(ClosedGroupOperation.Subscribe, closedGroup, publicKey)
|
||||
}
|
||||
}
|
||||
|
||||
fun performOperation(operation: ClosedGroupOperation, closedGroupPublicKey: String, publicKey: String) {
|
||||
if (!TextSecurePreferences.isUsingFCM(context)) { return }
|
||||
val parameters = mapOf( "closedGroupPublicKey" to closedGroupPublicKey, "pubKey" to publicKey )
|
||||
val url = "$server/${operation.rawValue}"
|
||||
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
|
||||
val request = Request.Builder().url(url).post(body)
|
||||
retryIfNeeded(maxRetryCount) {
|
||||
OnionRequestAPI.sendOnionRequest(request.build(), server, serverPublicKey, Version.V2).map { response ->
|
||||
val code = response.info["code"] as? Int
|
||||
if (code == null || code == 0) {
|
||||
Log.d("Loki", "Couldn't subscribe/unsubscribe closed group: $closedGroupPublicKey due to error: ${response.info["message"] as? String ?: "null"}.")
|
||||
}
|
||||
}.fail { exception ->
|
||||
Log.d("Loki", "Couldn't subscribe/unsubscribe closed group: $closedGroupPublicKey due to error: ${exception}.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,141 @@
|
||||
package org.session.libsession.messaging.sending_receiving.notifications
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import nl.komponents.kovenant.Promise
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.snode.OnionRequestAPI
|
||||
import org.session.libsession.snode.OnionResponse
|
||||
import org.session.libsession.snode.Version
|
||||
import org.session.libsession.utilities.Device
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.utilities.JsonUtil
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.session.libsignal.utilities.emptyPromise
|
||||
import org.session.libsignal.utilities.retryIfNeeded
|
||||
import org.session.libsignal.utilities.sideEffect
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
object PushRegistryV1 {
|
||||
private val TAG = PushRegistryV1::class.java.name
|
||||
|
||||
val context = MessagingModuleConfiguration.shared.context
|
||||
private const val maxRetryCount = 4
|
||||
|
||||
private val server = Server.LEGACY
|
||||
|
||||
fun register(
|
||||
device: Device,
|
||||
isPushEnabled: Boolean = TextSecurePreferences.isPushEnabled(context),
|
||||
token: String? = TextSecurePreferences.getPushToken(context),
|
||||
publicKey: String? = TextSecurePreferences.getLocalNumber(context),
|
||||
legacyGroupPublicKeys: Collection<String> = MessagingModuleConfiguration.shared.storage.getAllClosedGroupPublicKeys()
|
||||
): Promise<*, Exception> = when {
|
||||
isPushEnabled -> retryIfNeeded(maxRetryCount) {
|
||||
Log.d(TAG, "register() called")
|
||||
doRegister(token, publicKey, device, legacyGroupPublicKeys)
|
||||
} fail { exception ->
|
||||
Log.d(TAG, "Couldn't register for FCM due to error", exception)
|
||||
}
|
||||
|
||||
else -> emptyPromise()
|
||||
}
|
||||
|
||||
private fun doRegister(token: String?, publicKey: String?, device: Device, legacyGroupPublicKeys: Collection<String>): Promise<*, Exception> {
|
||||
Log.d(TAG, "doRegister() called")
|
||||
|
||||
token ?: return emptyPromise()
|
||||
publicKey ?: return emptyPromise()
|
||||
|
||||
val parameters = mapOf(
|
||||
"token" to token,
|
||||
"pubKey" to publicKey,
|
||||
"device" to device.value,
|
||||
"legacyGroupPublicKeys" to legacyGroupPublicKeys
|
||||
)
|
||||
|
||||
val url = "${server.url}/register_legacy_groups_only"
|
||||
val body = RequestBody.create(
|
||||
MediaType.get("application/json"),
|
||||
JsonUtil.toJson(parameters)
|
||||
)
|
||||
val request = Request.Builder().url(url).post(body).build()
|
||||
|
||||
return sendOnionRequest(request) sideEffect { response ->
|
||||
when (response.code) {
|
||||
null, 0 -> throw Exception("error: ${response.message}.")
|
||||
}
|
||||
} success {
|
||||
Log.d(TAG, "registerV1 success")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister push notifications for 1-1 conversations as this is now done in FirebasePushManager.
|
||||
*/
|
||||
fun unregister(): Promise<*, Exception> {
|
||||
Log.d(TAG, "unregisterV1 requested")
|
||||
|
||||
val token = TextSecurePreferences.getPushToken(context) ?: emptyPromise()
|
||||
|
||||
return retryIfNeeded(maxRetryCount) {
|
||||
val parameters = mapOf("token" to token)
|
||||
val url = "${server.url}/unregister"
|
||||
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
|
||||
val request = Request.Builder().url(url).post(body).build()
|
||||
|
||||
sendOnionRequest(request) success {
|
||||
when (it.code) {
|
||||
null, 0 -> Log.d(TAG, "error: ${it.message}.")
|
||||
else -> Log.d(TAG, "unregisterV1 success")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy Closed Groups
|
||||
|
||||
fun subscribeGroup(
|
||||
closedGroupPublicKey: String,
|
||||
isPushEnabled: Boolean = TextSecurePreferences.isPushEnabled(context),
|
||||
publicKey: String = MessagingModuleConfiguration.shared.storage.getUserPublicKey()!!
|
||||
) = if (isPushEnabled) {
|
||||
performGroupOperation("subscribe_closed_group", closedGroupPublicKey, publicKey)
|
||||
} else emptyPromise()
|
||||
|
||||
fun unsubscribeGroup(
|
||||
closedGroupPublicKey: String,
|
||||
isPushEnabled: Boolean = TextSecurePreferences.isPushEnabled(context),
|
||||
publicKey: String = MessagingModuleConfiguration.shared.storage.getUserPublicKey()!!
|
||||
) = if (isPushEnabled) {
|
||||
performGroupOperation("unsubscribe_closed_group", closedGroupPublicKey, publicKey)
|
||||
} else emptyPromise()
|
||||
|
||||
private fun performGroupOperation(
|
||||
operation: String,
|
||||
closedGroupPublicKey: String,
|
||||
publicKey: String
|
||||
): Promise<*, Exception> {
|
||||
val parameters = mapOf("closedGroupPublicKey" to closedGroupPublicKey, "pubKey" to publicKey)
|
||||
val url = "${server.url}/$operation"
|
||||
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
|
||||
val request = Request.Builder().url(url).post(body).build()
|
||||
|
||||
return retryIfNeeded(maxRetryCount) {
|
||||
sendOnionRequest(request) sideEffect {
|
||||
when (it.code) {
|
||||
0, null -> throw Exception(it.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendOnionRequest(request: Request): Promise<OnionResponse, Exception> = OnionRequestAPI.sendOnionRequest(
|
||||
request,
|
||||
server.url,
|
||||
server.publicKey,
|
||||
Version.V2
|
||||
)
|
||||
}
|
@@ -0,0 +1,6 @@
|
||||
package org.session.libsession.messaging.sending_receiving.notifications
|
||||
|
||||
enum class Server(val url: String, val publicKey: String) {
|
||||
LATEST("https://push.getsession.org", "d7557fe563e2610de876c0ac7341b62f3c82d5eea4b62c702392ea4368f51b3b"),
|
||||
LEGACY("https://live.apns.getsession.org", "642a6585919742e5a2d4dc51244964fbcd8bcab2b75612407de58b810740d049")
|
||||
}
|
@@ -54,10 +54,9 @@ class ClosedGroupPollerV2 {
|
||||
setUpPolling(groupPublicKey)
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val allGroupPublicKeys = storage.getAllClosedGroupPublicKeys()
|
||||
allGroupPublicKeys.iterator().forEach { stopPolling(it) }
|
||||
fun stopAll() {
|
||||
futures.forEach { it.value.cancel(false) }
|
||||
isPolling.forEach { isPolling[it.key] = false }
|
||||
}
|
||||
|
||||
fun stopPolling(groupPublicKey: String) {
|
||||
@@ -80,7 +79,12 @@ class ClosedGroupPollerV2 {
|
||||
// reasonable fake time interval to use instead.
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||
val threadID = storage.getThreadId(groupID) ?: return
|
||||
val threadID = storage.getThreadId(groupID)
|
||||
if (threadID == null) {
|
||||
Log.d("Loki", "Stopping group poller due to missing thread for closed group: $groupPublicKey.")
|
||||
stopPolling(groupPublicKey)
|
||||
return
|
||||
}
|
||||
val lastUpdated = storage.getLastUpdated(threadID)
|
||||
val timeSinceLastMessage = if (lastUpdated != -1L) Date().time - lastUpdated else 5 * 60 * 1000
|
||||
val minPollInterval = Companion.minPollInterval
|
||||
|
@@ -12,6 +12,7 @@ import org.session.libsession.messaging.jobs.MessageReceiveJob
|
||||
import org.session.libsession.messaging.jobs.MessageReceiveParameters
|
||||
import org.session.libsession.messaging.jobs.OpenGroupDeleteJob
|
||||
import org.session.libsession.messaging.jobs.TrimThreadJob
|
||||
import org.session.libsession.messaging.messages.Message
|
||||
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate
|
||||
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
||||
import org.session.libsession.messaging.open_groups.Endpoint
|
||||
@@ -30,6 +31,7 @@ import org.session.libsignal.protos.SignalServiceProtos
|
||||
import org.session.libsignal.utilities.Base64
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.session.libsignal.utilities.successBackground
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.ScheduledExecutorService
|
||||
import java.util.concurrent.ScheduledFuture
|
||||
import java.util.concurrent.TimeUnit
|
||||
@@ -39,15 +41,101 @@ class OpenGroupPoller(private val server: String, private val executorService: S
|
||||
var isCaughtUp = false
|
||||
var secondToLastJob: MessageReceiveJob? = null
|
||||
private var future: ScheduledFuture<*>? = null
|
||||
@Volatile private var runId: UUID = UUID.randomUUID()
|
||||
|
||||
companion object {
|
||||
private const val pollInterval: Long = 4000L
|
||||
const val maxInactivityPeriod = 14 * 24 * 60 * 60 * 1000
|
||||
|
||||
public fun handleRoomPollInfo(
|
||||
server: String,
|
||||
roomToken: String,
|
||||
pollInfo: OpenGroupApi.RoomPollInfo,
|
||||
createGroupIfMissingWithPublicKey: String? = null
|
||||
) {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val groupId = "$server.$roomToken"
|
||||
val dbGroupId = GroupUtil.getEncodedOpenGroupID(groupId.toByteArray())
|
||||
val existingOpenGroup = storage.getOpenGroup(roomToken, server)
|
||||
|
||||
// If we don't have an existing group and don't have a 'createGroupIfMissingWithPublicKey'
|
||||
// value then don't process the poll info
|
||||
val publicKey = existingOpenGroup?.publicKey ?: createGroupIfMissingWithPublicKey
|
||||
val name = pollInfo.details?.name ?: existingOpenGroup?.name
|
||||
val infoUpdates = pollInfo.details?.infoUpdates ?: existingOpenGroup?.infoUpdates
|
||||
|
||||
if (publicKey == null) return
|
||||
|
||||
val openGroup = OpenGroup(
|
||||
server = server,
|
||||
room = pollInfo.token,
|
||||
name = name ?: "",
|
||||
publicKey = publicKey,
|
||||
imageId = (pollInfo.details?.imageId ?: existingOpenGroup?.imageId),
|
||||
canWrite = pollInfo.write,
|
||||
infoUpdates = infoUpdates ?: 0
|
||||
)
|
||||
// - Open Group changes
|
||||
storage.updateOpenGroup(openGroup)
|
||||
|
||||
// - User Count
|
||||
storage.setUserCount(roomToken, server, pollInfo.activeUsers)
|
||||
|
||||
// - Moderators
|
||||
pollInfo.details?.moderators?.let { moderatorList ->
|
||||
storage.setGroupMemberRoles(moderatorList.map {
|
||||
GroupMember(groupId, it, GroupMemberRole.MODERATOR)
|
||||
})
|
||||
}
|
||||
pollInfo.details?.hiddenModerators?.let { moderatorList ->
|
||||
storage.setGroupMemberRoles(moderatorList.map {
|
||||
GroupMember(groupId, it, GroupMemberRole.HIDDEN_MODERATOR)
|
||||
})
|
||||
}
|
||||
// - Admins
|
||||
pollInfo.details?.admins?.let { moderatorList ->
|
||||
storage.setGroupMemberRoles(moderatorList.map {
|
||||
GroupMember(groupId, it, GroupMemberRole.ADMIN)
|
||||
})
|
||||
}
|
||||
pollInfo.details?.hiddenAdmins?.let { moderatorList ->
|
||||
storage.setGroupMemberRoles(moderatorList.map {
|
||||
GroupMember(groupId, it, GroupMemberRole.HIDDEN_ADMIN)
|
||||
})
|
||||
}
|
||||
|
||||
// Update the group avatar
|
||||
if (
|
||||
(
|
||||
pollInfo.details != null &&
|
||||
pollInfo.details.imageId != null && (
|
||||
pollInfo.details.imageId != existingOpenGroup?.imageId ||
|
||||
!storage.hasDownloadedProfilePicture(dbGroupId)
|
||||
) &&
|
||||
storage.getGroupAvatarDownloadJob(openGroup.server, openGroup.room, pollInfo.details.imageId) == null
|
||||
) || (
|
||||
pollInfo.details == null &&
|
||||
existingOpenGroup?.imageId != null &&
|
||||
!storage.hasDownloadedProfilePicture(dbGroupId) &&
|
||||
storage.getGroupAvatarDownloadJob(openGroup.server, openGroup.room, existingOpenGroup.imageId) == null
|
||||
)
|
||||
) {
|
||||
JobQueue.shared.add(GroupAvatarDownloadJob(server, roomToken, openGroup.imageId))
|
||||
}
|
||||
else if (
|
||||
pollInfo.details != null &&
|
||||
pollInfo.details.imageId == null &&
|
||||
existingOpenGroup?.imageId != null
|
||||
) {
|
||||
storage.removeProfilePicture(dbGroupId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun startIfNeeded() {
|
||||
if (hasStarted) { return }
|
||||
hasStarted = true
|
||||
runId = UUID.randomUUID()
|
||||
future = executorService?.schedule(::poll, 0, TimeUnit.MILLISECONDS)
|
||||
}
|
||||
|
||||
@@ -57,9 +145,10 @@ class OpenGroupPoller(private val server: String, private val executorService: S
|
||||
}
|
||||
|
||||
fun poll(isPostCapabilitiesRetry: Boolean = false): Promise<Unit, Exception> {
|
||||
val currentRunId = runId
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val rooms = storage.getAllOpenGroups().values.filter { it.server == server }.map { it.room }
|
||||
rooms.forEach { downloadGroupAvatarIfNeeded(it) }
|
||||
|
||||
return OpenGroupApi.poll(rooms, server).successBackground { responses ->
|
||||
responses.filterNot { it.body == null }.forEach { response ->
|
||||
when (response.endpoint) {
|
||||
@@ -81,27 +170,36 @@ class OpenGroupPoller(private val server: String, private val executorService: S
|
||||
is Endpoint.Outbox, is Endpoint.OutboxSince -> {
|
||||
handleDirectMessages(server, true, response.body as List<OpenGroupApi.DirectMessage>)
|
||||
}
|
||||
else -> { /* We don't care about the result of any other calls (won't be polled for) */}
|
||||
}
|
||||
if (secondToLastJob == null && !isCaughtUp) {
|
||||
isCaughtUp = true
|
||||
}
|
||||
}
|
||||
executorService?.schedule(this@OpenGroupPoller::poll, pollInterval, TimeUnit.MILLISECONDS)
|
||||
|
||||
// Only poll again if it's the same poller run
|
||||
if (currentRunId == runId) {
|
||||
future = executorService?.schedule(this@OpenGroupPoller::poll, pollInterval, TimeUnit.MILLISECONDS)
|
||||
}
|
||||
}.fail {
|
||||
updateCapabilitiesIfNeeded(isPostCapabilitiesRetry, it)
|
||||
updateCapabilitiesIfNeeded(isPostCapabilitiesRetry, currentRunId, it)
|
||||
}.map { }
|
||||
}
|
||||
|
||||
private fun updateCapabilitiesIfNeeded(isPostCapabilitiesRetry: Boolean, exception: Exception) {
|
||||
private fun updateCapabilitiesIfNeeded(isPostCapabilitiesRetry: Boolean, currentRunId: UUID, exception: Exception) {
|
||||
if (exception is OnionRequestAPI.HTTPRequestFailedBlindingRequiredException) {
|
||||
if (!isPostCapabilitiesRetry) {
|
||||
OpenGroupApi.getCapabilities(server).map {
|
||||
handleCapabilities(server, it)
|
||||
}
|
||||
executorService?.schedule({ poll(isPostCapabilitiesRetry = true) }, pollInterval, TimeUnit.MILLISECONDS)
|
||||
|
||||
// Only poll again if it's the same poller run
|
||||
if (currentRunId == runId) {
|
||||
future = executorService?.schedule({ poll(isPostCapabilitiesRetry = true) }, pollInterval, TimeUnit.MILLISECONDS)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
executorService?.schedule(this@OpenGroupPoller::poll, pollInterval, TimeUnit.MILLISECONDS)
|
||||
} else if (currentRunId == runId) {
|
||||
future = executorService?.schedule(this@OpenGroupPoller::poll, pollInterval, TimeUnit.MILLISECONDS)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,54 +207,7 @@ class OpenGroupPoller(private val server: String, private val executorService: S
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
storage.setServerCapabilities(server, capabilities.capabilities)
|
||||
}
|
||||
|
||||
private fun handleRoomPollInfo(
|
||||
server: String,
|
||||
roomToken: String,
|
||||
pollInfo: OpenGroupApi.RoomPollInfo
|
||||
) {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val groupId = "$server.$roomToken"
|
||||
|
||||
val existingOpenGroup = storage.getOpenGroup(roomToken, server)
|
||||
val publicKey = existingOpenGroup?.publicKey ?: return
|
||||
val openGroup = OpenGroup(
|
||||
server = server,
|
||||
room = pollInfo.token,
|
||||
name = pollInfo.details?.name ?: "",
|
||||
infoUpdates = pollInfo.details?.infoUpdates ?: 0,
|
||||
publicKey = publicKey,
|
||||
)
|
||||
// - Open Group changes
|
||||
storage.updateOpenGroup(openGroup)
|
||||
|
||||
// - User Count
|
||||
storage.setUserCount(roomToken, server, pollInfo.activeUsers)
|
||||
|
||||
// - Moderators
|
||||
pollInfo.details?.moderators?.let { moderatorList ->
|
||||
storage.setGroupMemberRoles(moderatorList.map {
|
||||
GroupMember(groupId, it, GroupMemberRole.MODERATOR)
|
||||
})
|
||||
}
|
||||
pollInfo.details?.hiddenModerators?.let { moderatorList ->
|
||||
storage.setGroupMemberRoles(moderatorList.map {
|
||||
GroupMember(groupId, it, GroupMemberRole.HIDDEN_MODERATOR)
|
||||
})
|
||||
}
|
||||
// - Admins
|
||||
pollInfo.details?.admins?.let { moderatorList ->
|
||||
storage.setGroupMemberRoles(moderatorList.map {
|
||||
GroupMember(groupId, it, GroupMemberRole.ADMIN)
|
||||
})
|
||||
}
|
||||
pollInfo.details?.hiddenAdmins?.let { moderatorList ->
|
||||
storage.setGroupMemberRoles(moderatorList.map {
|
||||
GroupMember(groupId, it, GroupMemberRole.HIDDEN_ADMIN)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun handleMessages(
|
||||
server: String,
|
||||
roomToken: String,
|
||||
@@ -211,7 +262,8 @@ class OpenGroupPoller(private val server: String, private val executorService: S
|
||||
null,
|
||||
fromOutbox,
|
||||
if (fromOutbox) it.recipient else it.sender,
|
||||
serverPublicKey
|
||||
serverPublicKey,
|
||||
emptySet() // this shouldn't be necessary as we are polling open groups here
|
||||
)
|
||||
if (fromOutbox) {
|
||||
val mapping = mappingCache[it.recipient] ?: storage.getOrCreateBlindedIdMapping(
|
||||
@@ -228,7 +280,8 @@ class OpenGroupPoller(private val server: String, private val executorService: S
|
||||
}
|
||||
mappingCache[it.recipient] = mapping
|
||||
}
|
||||
MessageReceiver.handle(message, proto, null)
|
||||
val threadId = Message.getThreadId(message, null, MessagingModuleConfiguration.shared.storage, false)
|
||||
MessageReceiver.handle(message, proto, threadId ?: -1, null)
|
||||
} catch (e: Exception) {
|
||||
Log.e("Loki", "Couldn't handle direct message", e)
|
||||
}
|
||||
@@ -284,16 +337,4 @@ class OpenGroupPoller(private val server: String, private val executorService: S
|
||||
JobQueue.shared.add(deleteJob)
|
||||
}
|
||||
}
|
||||
|
||||
private fun downloadGroupAvatarIfNeeded(room: String) {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
if (storage.getGroupAvatarDownloadJob(server, room) != null) return
|
||||
val groupId = GroupUtil.getEncodedOpenGroupID("$server.$room".toByteArray())
|
||||
storage.getGroup(groupId)?.let {
|
||||
if (System.currentTimeMillis() > it.updatedTimestamp + TimeUnit.DAYS.toMillis(7)) {
|
||||
JobQueue.shared.add(GroupAvatarDownloadJob(room, server))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -1,5 +1,14 @@
|
||||
package org.session.libsession.messaging.sending_receiving.pollers
|
||||
|
||||
import android.util.SparseArray
|
||||
import androidx.core.util.valueIterator
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import network.loki.messenger.libsession_util.ConfigBase
|
||||
import network.loki.messenger.libsession_util.Contacts
|
||||
import network.loki.messenger.libsession_util.ConversationVolatileConfig
|
||||
import network.loki.messenger.libsession_util.UserGroupsConfig
|
||||
import network.loki.messenger.libsession_util.UserProfile
|
||||
import nl.komponents.kovenant.Deferred
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.deferred
|
||||
@@ -10,17 +19,23 @@ import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.jobs.BatchMessageReceiveJob
|
||||
import org.session.libsession.messaging.jobs.JobQueue
|
||||
import org.session.libsession.messaging.jobs.MessageReceiveParameters
|
||||
import org.session.libsession.messaging.messages.control.SharedConfigurationMessage
|
||||
import org.session.libsession.messaging.sending_receiving.MessageReceiver
|
||||
import org.session.libsession.snode.RawResponse
|
||||
import org.session.libsession.snode.SnodeAPI
|
||||
import org.session.libsession.snode.SnodeModule
|
||||
import org.session.libsession.utilities.ConfigFactoryProtocol
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.session.libsignal.utilities.Namespace
|
||||
import org.session.libsignal.utilities.Snode
|
||||
import java.security.SecureRandom
|
||||
import java.util.Timer
|
||||
import java.util.TimerTask
|
||||
import kotlin.time.Duration.Companion.days
|
||||
|
||||
private class PromiseCanceledException : Exception("Promise canceled.")
|
||||
|
||||
class Poller {
|
||||
class Poller(private val configFactory: ConfigFactoryProtocol, debounceTimer: Timer) {
|
||||
var userPublicKey = MessagingModuleConfiguration.shared.storage.getUserPublicKey() ?: ""
|
||||
private var hasStarted: Boolean = false
|
||||
private val usedSnodes: MutableSet<Snode> = mutableSetOf()
|
||||
@@ -97,23 +112,162 @@ class Poller {
|
||||
}
|
||||
}
|
||||
|
||||
private fun processPersonalMessages(snode: Snode, rawMessages: RawResponse) {
|
||||
val messages = SnodeAPI.parseRawMessagesResponse(rawMessages, snode, userPublicKey)
|
||||
val parameters = messages.map { (envelope, serverHash) ->
|
||||
MessageReceiveParameters(envelope.toByteArray(), serverHash = serverHash)
|
||||
}
|
||||
parameters.chunked(BatchMessageReceiveJob.BATCH_DEFAULT_NUMBER).forEach { chunk ->
|
||||
val job = BatchMessageReceiveJob(chunk)
|
||||
JobQueue.shared.add(job)
|
||||
}
|
||||
}
|
||||
|
||||
private fun processConfig(snode: Snode, rawMessages: RawResponse, namespace: Int, forConfigObject: ConfigBase?) {
|
||||
if (forConfigObject == null) return
|
||||
|
||||
val messages = SnodeAPI.parseRawMessagesResponse(
|
||||
rawMessages,
|
||||
snode,
|
||||
userPublicKey,
|
||||
namespace,
|
||||
updateLatestHash = true,
|
||||
updateStoredHashes = true,
|
||||
)
|
||||
|
||||
if (messages.isEmpty()) {
|
||||
// no new messages to process
|
||||
return
|
||||
}
|
||||
|
||||
var latestMessageTimestamp: Long? = null
|
||||
messages.forEach { (envelope, hash) ->
|
||||
try {
|
||||
val (message, _) = MessageReceiver.parse(data = envelope.toByteArray(),
|
||||
// assume no groups in personal poller messages
|
||||
openGroupServerID = null, currentClosedGroups = emptySet()
|
||||
)
|
||||
// sanity checks
|
||||
if (message !is SharedConfigurationMessage) {
|
||||
Log.w("Loki", "shared config message handled in configs wasn't SharedConfigurationMessage but was ${message.javaClass.simpleName}")
|
||||
return@forEach
|
||||
}
|
||||
val merged = forConfigObject.merge(hash!! to message.data).firstOrNull { it == hash }
|
||||
if (merged != null) {
|
||||
// We successfully merged the hash, we can now update the timestamp
|
||||
latestMessageTimestamp = if ((message.sentTimestamp ?: 0L) > (latestMessageTimestamp ?: 0L)) { message.sentTimestamp } else { latestMessageTimestamp }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("Loki", e)
|
||||
}
|
||||
}
|
||||
// process new results
|
||||
// latestMessageTimestamp should always be non-null if the config object needs dump
|
||||
if (forConfigObject.needsDump() && latestMessageTimestamp != null) {
|
||||
configFactory.persist(forConfigObject, latestMessageTimestamp ?: SnodeAPI.nowWithOffset)
|
||||
}
|
||||
}
|
||||
|
||||
private fun poll(snode: Snode, deferred: Deferred<Unit, Exception>): Promise<Unit, Exception> {
|
||||
if (!hasStarted) { return Promise.ofFail(PromiseCanceledException()) }
|
||||
return SnodeAPI.getRawMessages(snode, userPublicKey).bind { rawResponse ->
|
||||
isCaughtUp = true
|
||||
if (deferred.promise.isDone()) {
|
||||
task { Unit } // The long polling connection has been canceled; don't recurse
|
||||
} else {
|
||||
val messages = SnodeAPI.parseRawMessagesResponse(rawResponse, snode, userPublicKey)
|
||||
val parameters = messages.map { (envelope, serverHash) ->
|
||||
MessageReceiveParameters(envelope.toByteArray(), serverHash = serverHash)
|
||||
return task {
|
||||
runBlocking(Dispatchers.IO) {
|
||||
val requestSparseArray = SparseArray<SnodeAPI.SnodeBatchRequestInfo>()
|
||||
// get messages
|
||||
SnodeAPI.buildAuthenticatedRetrieveBatchRequest(snode, userPublicKey, maxSize = -2)!!.also { personalMessages ->
|
||||
// namespaces here should always be set
|
||||
requestSparseArray[personalMessages.namespace!!] = personalMessages
|
||||
}
|
||||
parameters.chunked(BatchMessageReceiveJob.BATCH_DEFAULT_NUMBER).forEach { chunk ->
|
||||
val job = BatchMessageReceiveJob(chunk)
|
||||
JobQueue.shared.add(job)
|
||||
// get the latest convo info volatile
|
||||
val hashesToExtend = mutableSetOf<String>()
|
||||
configFactory.getUserConfigs().mapNotNull { config ->
|
||||
hashesToExtend += config.currentHashes()
|
||||
SnodeAPI.buildAuthenticatedRetrieveBatchRequest(
|
||||
snode, userPublicKey,
|
||||
config.configNamespace(),
|
||||
maxSize = -8
|
||||
)
|
||||
}.forEach { request ->
|
||||
// namespaces here should always be set
|
||||
requestSparseArray[request.namespace!!] = request
|
||||
}
|
||||
|
||||
poll(snode, deferred)
|
||||
val requests =
|
||||
requestSparseArray.valueIterator().asSequence().toMutableList()
|
||||
|
||||
if (hashesToExtend.isNotEmpty()) {
|
||||
SnodeAPI.buildAuthenticatedAlterTtlBatchRequest(
|
||||
messageHashes = hashesToExtend.toList(),
|
||||
publicKey = userPublicKey,
|
||||
newExpiry = SnodeAPI.nowWithOffset + 14.days.inWholeMilliseconds,
|
||||
extend = true
|
||||
)?.let { extensionRequest ->
|
||||
requests += extensionRequest
|
||||
}
|
||||
}
|
||||
|
||||
SnodeAPI.getRawBatchResponse(snode, userPublicKey, requests).bind { rawResponses ->
|
||||
isCaughtUp = true
|
||||
if (deferred.promise.isDone()) {
|
||||
return@bind Promise.ofSuccess(Unit)
|
||||
} else {
|
||||
val responseList = (rawResponses["results"] as List<RawResponse>)
|
||||
// in case we had null configs, the array won't be fully populated
|
||||
// index of the sparse array key iterator should be the request index, with the key being the namespace
|
||||
listOfNotNull(
|
||||
configFactory.user?.configNamespace(),
|
||||
configFactory.contacts?.configNamespace(),
|
||||
configFactory.userGroups?.configNamespace(),
|
||||
configFactory.convoVolatile?.configNamespace()
|
||||
).map {
|
||||
it to requestSparseArray.indexOfKey(it)
|
||||
}.filter { (_, i) -> i >= 0 }.forEach { (key, requestIndex) ->
|
||||
responseList.getOrNull(requestIndex)?.let { rawResponse ->
|
||||
if (rawResponse["code"] as? Int != 200) {
|
||||
Log.e("Loki", "Batch sub-request had non-200 response code, returned code ${(rawResponse["code"] as? Int) ?: "[unknown]"}")
|
||||
return@forEach
|
||||
}
|
||||
val body = rawResponse["body"] as? RawResponse
|
||||
if (body == null) {
|
||||
Log.e("Loki", "Batch sub-request didn't contain a body")
|
||||
return@forEach
|
||||
}
|
||||
if (key == Namespace.DEFAULT) {
|
||||
return@forEach // continue, skip default namespace
|
||||
} else {
|
||||
when (ConfigBase.kindFor(key)) {
|
||||
UserProfile::class.java -> processConfig(snode, body, key, configFactory.user)
|
||||
Contacts::class.java -> processConfig(snode, body, key, configFactory.contacts)
|
||||
ConversationVolatileConfig::class.java -> processConfig(snode, body, key, configFactory.convoVolatile)
|
||||
UserGroupsConfig::class.java -> processConfig(snode, body, key, configFactory.userGroups)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// the first response will be the personal messages (we want these to be processed after config messages)
|
||||
val personalResponseIndex = requestSparseArray.indexOfKey(Namespace.DEFAULT)
|
||||
if (personalResponseIndex >= 0) {
|
||||
responseList.getOrNull(personalResponseIndex)?.let { rawResponse ->
|
||||
if (rawResponse["code"] as? Int != 200) {
|
||||
Log.e("Loki", "Batch sub-request for personal messages had non-200 response code, returned code ${(rawResponse["code"] as? Int) ?: "[unknown]"}")
|
||||
} else {
|
||||
val body = rawResponse["body"] as? RawResponse
|
||||
if (body == null) {
|
||||
Log.e("Loki", "Batch sub-request for personal messages didn't contain a body")
|
||||
} else {
|
||||
processPersonalMessages(snode, body)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
poll(snode, deferred)
|
||||
}
|
||||
}.fail {
|
||||
Log.e("Loki", "Failed to get raw batch response", it)
|
||||
poll(snode, deferred)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -14,7 +14,7 @@ import org.whispersystems.curve25519.Curve25519
|
||||
import kotlin.experimental.xor
|
||||
|
||||
object SodiumUtilities {
|
||||
private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) }
|
||||
val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) }
|
||||
private val curve by lazy { Curve25519.getInstance(Curve25519.BEST) }
|
||||
|
||||
private const val SCALAR_LENGTH: Int = 32 // crypto_core_ed25519_scalarbytes
|
||||
@@ -205,7 +205,7 @@ object SodiumUtilities {
|
||||
}
|
||||
|
||||
fun decrypt(ciphertext: ByteArray, decryptionKey: ByteArray, nonce: ByteArray): ByteArray? {
|
||||
val plaintextSize = ciphertext.size - AEAD.CHACHA20POLY1305_ABYTES
|
||||
val plaintextSize = ciphertext.size - AEAD.XCHACHA20POLY1305_IETF_ABYTES
|
||||
val plaintext = ByteArray(plaintextSize)
|
||||
return if (sodium.cryptoAeadXChaCha20Poly1305IetfDecrypt(
|
||||
plaintext,
|
||||
|
@@ -1,121 +1,111 @@
|
||||
package org.session.libsession.messaging.utilities
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import org.session.libsession.R
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.calls.CallMessageType
|
||||
import org.session.libsession.messaging.calls.CallMessageType.CALL_FIRST_MISSED
|
||||
import org.session.libsession.messaging.calls.CallMessageType.CALL_INCOMING
|
||||
import org.session.libsession.messaging.calls.CallMessageType.CALL_MISSED
|
||||
import org.session.libsession.messaging.calls.CallMessageType.CALL_OUTGOING
|
||||
import org.session.libsession.messaging.contacts.Contact
|
||||
import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage
|
||||
import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage.Kind.MEDIA_SAVED
|
||||
import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage.Kind.SCREENSHOT
|
||||
import org.session.libsession.utilities.ExpirationUtil
|
||||
import org.session.libsession.utilities.getExpirationTypeDisplayValue
|
||||
import org.session.libsession.utilities.truncateIdForDisplay
|
||||
|
||||
object UpdateMessageBuilder {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
|
||||
fun buildGroupUpdateMessage(context: Context, updateMessageData: UpdateMessageData, sender: String? = null, isOutgoing: Boolean = false): String {
|
||||
var message = ""
|
||||
val updateData = updateMessageData.kind ?: return message
|
||||
if (!isOutgoing && sender == null) return message
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val senderName: String = if (!isOutgoing) {
|
||||
storage.getContactWithSessionID(sender!!)?.displayName(Contact.ContactContext.REGULAR) ?: sender
|
||||
} else { context.getString(R.string.MessageRecord_you) }
|
||||
private fun getSenderName(senderId: String) = storage.getContactWithSessionID(senderId)
|
||||
?.displayName(Contact.ContactContext.REGULAR)
|
||||
?: truncateIdForDisplay(senderId)
|
||||
|
||||
when (updateData) {
|
||||
fun buildGroupUpdateMessage(context: Context, updateMessageData: UpdateMessageData, senderId: String? = null, isOutgoing: Boolean = false): String {
|
||||
val updateData = updateMessageData.kind
|
||||
if (updateData == null || !isOutgoing && senderId == null) return ""
|
||||
val senderName: String = if (isOutgoing) context.getString(R.string.MessageRecord_you)
|
||||
else getSenderName(senderId!!)
|
||||
|
||||
return when (updateData) {
|
||||
is UpdateMessageData.Kind.GroupCreation -> {
|
||||
message = if (isOutgoing) {
|
||||
context.getString(R.string.MessageRecord_you_created_a_new_group)
|
||||
} else {
|
||||
context.getString(R.string.MessageRecord_s_added_you_to_the_group, senderName)
|
||||
}
|
||||
if (isOutgoing) context.getString(R.string.MessageRecord_you_created_a_new_group)
|
||||
else context.getString(R.string.MessageRecord_s_added_you_to_the_group, senderName)
|
||||
}
|
||||
is UpdateMessageData.Kind.GroupNameChange -> {
|
||||
message = if (isOutgoing) {
|
||||
context.getString(R.string.MessageRecord_you_renamed_the_group_to_s, updateData.name)
|
||||
} else {
|
||||
context.getString(R.string.MessageRecord_s_renamed_the_group_to_s, senderName, updateData.name)
|
||||
}
|
||||
if (isOutgoing) context.getString(R.string.MessageRecord_you_renamed_the_group_to_s, updateData.name)
|
||||
else context.getString(R.string.MessageRecord_s_renamed_the_group_to_s, senderName, updateData.name)
|
||||
}
|
||||
is UpdateMessageData.Kind.GroupMemberAdded -> {
|
||||
val members = updateData.updatedMembers.joinToString(", ") {
|
||||
storage.getContactWithSessionID(it)?.displayName(Contact.ContactContext.REGULAR) ?: it
|
||||
}
|
||||
message = if (isOutgoing) {
|
||||
context.getString(R.string.MessageRecord_you_added_s_to_the_group, members)
|
||||
} else {
|
||||
context.getString(R.string.MessageRecord_s_added_s_to_the_group, senderName, members)
|
||||
}
|
||||
val members = updateData.updatedMembers.joinToString(", ", transform = ::getSenderName)
|
||||
if (isOutgoing) context.getString(R.string.MessageRecord_you_added_s_to_the_group, members)
|
||||
else context.getString(R.string.MessageRecord_s_added_s_to_the_group, senderName, members)
|
||||
}
|
||||
is UpdateMessageData.Kind.GroupMemberRemoved -> {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val userPublicKey = storage.getUserPublicKey()!!
|
||||
// 1st case: you are part of the removed members
|
||||
message = if (userPublicKey in updateData.updatedMembers) {
|
||||
if (isOutgoing) {
|
||||
context.getString(R.string.MessageRecord_left_group)
|
||||
} else {
|
||||
context.getString(R.string.MessageRecord_you_were_removed_from_the_group)
|
||||
}
|
||||
return if (userPublicKey in updateData.updatedMembers) {
|
||||
if (isOutgoing) context.getString(R.string.MessageRecord_left_group)
|
||||
else context.getString(R.string.MessageRecord_you_were_removed_from_the_group)
|
||||
} else {
|
||||
// 2nd case: you are not part of the removed members
|
||||
val members = updateData.updatedMembers.joinToString(", ") {
|
||||
storage.getContactWithSessionID(it)?.displayName(Contact.ContactContext.REGULAR) ?: it
|
||||
}
|
||||
if (isOutgoing) {
|
||||
context.getString(R.string.MessageRecord_you_removed_s_from_the_group, members)
|
||||
} else {
|
||||
context.getString(R.string.MessageRecord_s_removed_s_from_the_group, senderName, members)
|
||||
}
|
||||
val members = updateData.updatedMembers.joinToString(", ", transform = ::getSenderName)
|
||||
if (isOutgoing) context.getString(R.string.MessageRecord_you_removed_s_from_the_group, members)
|
||||
else context.getString(R.string.MessageRecord_s_removed_s_from_the_group, senderName, members)
|
||||
}
|
||||
}
|
||||
is UpdateMessageData.Kind.GroupMemberLeft -> {
|
||||
message = if (isOutgoing) {
|
||||
context.getString(R.string.MessageRecord_left_group)
|
||||
} else {
|
||||
context.getString(R.string.ConversationItem_group_action_left, senderName)
|
||||
}
|
||||
if (isOutgoing) context.getString(R.string.MessageRecord_left_group)
|
||||
else context.getString(R.string.ConversationItem_group_action_left, senderName)
|
||||
}
|
||||
else -> return ""
|
||||
}
|
||||
return message
|
||||
}
|
||||
|
||||
fun buildExpirationTimerMessage(context: Context, duration: Long, sender: String? = null, isOutgoing: Boolean = false): String {
|
||||
if (!isOutgoing && sender == null) return ""
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val senderName: String? = if (!isOutgoing) {
|
||||
storage.getContactWithSessionID(sender!!)?.displayName(Contact.ContactContext.REGULAR) ?: sender
|
||||
} else { context.getString(R.string.MessageRecord_you) }
|
||||
fun buildExpirationTimerMessage(
|
||||
context: Context,
|
||||
duration: Long,
|
||||
isGroup: Boolean,
|
||||
senderId: String? = null,
|
||||
isOutgoing: Boolean = false,
|
||||
timestamp: Long,
|
||||
expireStarted: Long
|
||||
): String {
|
||||
if (!isOutgoing && senderId == null) return ""
|
||||
val senderName = if (isOutgoing) context.getString(R.string.MessageRecord_you) else getSenderName(senderId!!)
|
||||
return if (duration <= 0) {
|
||||
if (isOutgoing) context.getString(R.string.MessageRecord_you_disabled_disappearing_messages)
|
||||
else context.getString(R.string.MessageRecord_s_disabled_disappearing_messages, senderName)
|
||||
if (isOutgoing) context.getString(if (isGroup) R.string.MessageRecord_you_turned_off_disappearing_messages else R.string.MessageRecord_you_turned_off_disappearing_messages_1_on_1)
|
||||
else context.getString(if (isGroup) R.string.MessageRecord_s_turned_off_disappearing_messages else R.string.MessageRecord_s_turned_off_disappearing_messages_1_on_1, senderName)
|
||||
} else {
|
||||
val time = ExpirationUtil.getExpirationDisplayValue(context, duration.toInt())
|
||||
if (isOutgoing)context.getString(R.string.MessageRecord_you_set_disappearing_message_time_to_s, time)
|
||||
else context.getString(R.string.MessageRecord_s_set_disappearing_message_time_to_s, senderName, time)
|
||||
val action = context.getExpirationTypeDisplayValue(timestamp >= expireStarted)
|
||||
if (isOutgoing) context.getString(
|
||||
if (isGroup) R.string.MessageRecord_you_set_messages_to_disappear_s_after_s else R.string.MessageRecord_you_set_messages_to_disappear_s_after_s_1_on_1,
|
||||
time,
|
||||
action
|
||||
) else context.getString(
|
||||
if (isGroup) R.string.MessageRecord_s_set_messages_to_disappear_s_after_s else R.string.MessageRecord_s_set_messages_to_disappear_s_after_s_1_on_1,
|
||||
senderName,
|
||||
time,
|
||||
action
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun buildDataExtractionMessage(context: Context, kind: DataExtractionNotificationInfoMessage.Kind, sender: String? = null): String {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val senderName = storage.getContactWithSessionID(sender!!)?.displayName(Contact.ContactContext.REGULAR) ?: sender!!
|
||||
return when (kind) {
|
||||
DataExtractionNotificationInfoMessage.Kind.SCREENSHOT ->
|
||||
context.getString(R.string.MessageRecord_s_took_a_screenshot, senderName)
|
||||
DataExtractionNotificationInfoMessage.Kind.MEDIA_SAVED ->
|
||||
context.getString(R.string.MessageRecord_media_saved_by_s, senderName)
|
||||
}
|
||||
}
|
||||
fun buildDataExtractionMessage(context: Context, kind: DataExtractionNotificationInfoMessage.Kind, senderId: String? = null) = when (kind) {
|
||||
SCREENSHOT -> R.string.MessageRecord_s_took_a_screenshot
|
||||
MEDIA_SAVED -> R.string.MessageRecord_media_saved_by_s
|
||||
}.let { context.getString(it, getSenderName(senderId!!)) }
|
||||
|
||||
fun buildCallMessage(context: Context, type: CallMessageType, sender: String): String {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val senderName = storage.getContactWithSessionID(sender)?.displayName(Contact.ContactContext.REGULAR) ?: sender
|
||||
return when (type) {
|
||||
CallMessageType.CALL_MISSED ->
|
||||
context.getString(R.string.MessageRecord_missed_call_from, senderName)
|
||||
CallMessageType.CALL_INCOMING ->
|
||||
context.getString(R.string.MessageRecord_s_called_you, senderName)
|
||||
CallMessageType.CALL_OUTGOING ->
|
||||
context.getString(R.string.MessageRecord_called_s, senderName)
|
||||
CallMessageType.CALL_FIRST_MISSED ->
|
||||
context.getString(R.string.MessageRecord_missed_call_from, senderName)
|
||||
fun buildCallMessage(context: Context, type: CallMessageType, sender: String): String =
|
||||
when (type) {
|
||||
CALL_INCOMING -> R.string.MessageRecord_s_called_you
|
||||
CALL_OUTGOING -> R.string.MessageRecord_called_s
|
||||
CALL_MISSED, CALL_FIRST_MISSED -> R.string.MessageRecord_missed_call_from
|
||||
}.let {
|
||||
context.getString(it, storage.getContactWithSessionID(sender)?.displayName(Contact.ContactContext.REGULAR) ?: sender)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -26,6 +26,7 @@ import org.session.libsignal.utilities.ThreadUtils
|
||||
import org.session.libsignal.utilities.recover
|
||||
import org.session.libsignal.utilities.toHexString
|
||||
import java.util.Date
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
import kotlin.collections.set
|
||||
|
||||
private typealias Path = List<Snode>
|
||||
@@ -43,13 +44,27 @@ object OnionRequestAPI {
|
||||
private val snodeFailureCount = mutableMapOf<Snode, Int>()
|
||||
|
||||
var guardSnodes = setOf<Snode>()
|
||||
var _paths: AtomicReference<List<Path>?> = AtomicReference(null)
|
||||
var paths: List<Path> // Not a set to ensure we consistently show the same path to the user
|
||||
get() = database.getOnionRequestPaths()
|
||||
get() {
|
||||
val paths = _paths.get()
|
||||
|
||||
if (paths != null) { return paths }
|
||||
|
||||
// Storing this in an atomic variable as it was causing a number of background
|
||||
// ANRs when this value was accessed via the main thread after tapping on
|
||||
// a notification)
|
||||
val result = database.getOnionRequestPaths()
|
||||
_paths.set(result)
|
||||
return result
|
||||
}
|
||||
set(newValue) {
|
||||
if (newValue.isEmpty()) {
|
||||
database.clearOnionRequestPaths()
|
||||
_paths.set(null)
|
||||
} else {
|
||||
database.setOnionRequestPaths(newValue)
|
||||
_paths.set(newValue)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,8 +93,8 @@ object OnionRequestAPI {
|
||||
// endregion
|
||||
|
||||
class HTTPRequestFailedBlindingRequiredException(statusCode: Int, json: Map<*, *>, destination: String): HTTPRequestFailedAtDestinationException(statusCode, json, destination)
|
||||
open class HTTPRequestFailedAtDestinationException(val statusCode: Int, val json: Map<*, *>, val destination: String)
|
||||
: Exception("HTTP request failed at destination ($destination) with status code $statusCode.")
|
||||
open class HTTPRequestFailedAtDestinationException(statusCode: Int, json: Map<*, *>, val destination: String)
|
||||
: HTTP.HTTPRequestFailedException(statusCode, json, "HTTP request failed at destination ($destination) with status code $statusCode.")
|
||||
class InsufficientSnodesException : Exception("Couldn't find enough snodes to build a path.")
|
||||
|
||||
private data class OnionBuildingResult(
|
||||
@@ -404,6 +419,8 @@ object OnionRequestAPI {
|
||||
Log.d("Loki","Destination server returned ${exception.statusCode}")
|
||||
} else if (message == "Loki Server error") {
|
||||
Log.d("Loki", "message was $message")
|
||||
} else if (exception.statusCode == 404) {
|
||||
// 404 is probably file server missing a file, don't rebuild path or mark a snode as bad here
|
||||
} else { // Only drop snode/path if not receiving above two exception cases
|
||||
handleUnspecificError()
|
||||
}
|
||||
@@ -431,8 +448,8 @@ object OnionRequestAPI {
|
||||
val payloadData = JsonUtil.toJson(payload).toByteArray()
|
||||
return sendOnionRequest(Destination.Snode(snode), payloadData, version).recover { exception ->
|
||||
val error = when (exception) {
|
||||
is HTTP.HTTPRequestFailedException -> SnodeAPI.handleSnodeError(exception.statusCode, exception.json, snode, publicKey)
|
||||
is HTTPRequestFailedAtDestinationException -> SnodeAPI.handleSnodeError(exception.statusCode, exception.json, snode, publicKey)
|
||||
is HTTP.HTTPRequestFailedException -> SnodeAPI.handleSnodeError(exception.statusCode, exception.json, snode, publicKey)
|
||||
else -> null
|
||||
}
|
||||
if (error != null) { throw error }
|
||||
@@ -594,7 +611,7 @@ object OnionRequestAPI {
|
||||
}
|
||||
if (body["t"] != null) {
|
||||
val timestamp = body["t"] as Long
|
||||
val offset = timestamp - Date().time
|
||||
val offset = timestamp - System.currentTimeMillis()
|
||||
SnodeAPI.clockOffset = offset
|
||||
}
|
||||
if (body.containsKey("hf")) {
|
||||
@@ -669,4 +686,7 @@ enum class Version(val value: String) {
|
||||
data class OnionResponse(
|
||||
val info: Map<*, *>,
|
||||
val body: ByteArray? = null
|
||||
)
|
||||
) {
|
||||
val code: Int? get() = info["code"] as? Int
|
||||
val message: String? get() = info["message"] as? String
|
||||
}
|
||||
|
@@ -3,8 +3,6 @@
|
||||
package org.session.libsession.snode
|
||||
|
||||
import android.os.Build
|
||||
import com.goterl.lazysodium.LazySodiumAndroid
|
||||
import com.goterl.lazysodium.SodiumAndroid
|
||||
import com.goterl.lazysodium.exceptions.SodiumException
|
||||
import com.goterl.lazysodium.interfaces.GenericHash
|
||||
import com.goterl.lazysodium.interfaces.PwHash
|
||||
@@ -19,6 +17,7 @@ import nl.komponents.kovenant.functional.map
|
||||
import nl.komponents.kovenant.task
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.utilities.MessageWrapper
|
||||
import org.session.libsession.messaging.utilities.SodiumUtilities.sodium
|
||||
import org.session.libsignal.crypto.getRandomElement
|
||||
import org.session.libsignal.database.LokiAPIDatabaseProtocol
|
||||
import org.session.libsignal.protos.SignalServiceProtos
|
||||
@@ -28,12 +27,12 @@ import org.session.libsignal.utilities.HTTP
|
||||
import org.session.libsignal.utilities.Hex
|
||||
import org.session.libsignal.utilities.JsonUtil
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.session.libsignal.utilities.Namespace
|
||||
import org.session.libsignal.utilities.Snode
|
||||
import org.session.libsignal.utilities.ThreadUtils
|
||||
import org.session.libsignal.utilities.prettifiedDescription
|
||||
import org.session.libsignal.utilities.retryIfNeeded
|
||||
import java.security.SecureRandom
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import kotlin.collections.component1
|
||||
import kotlin.collections.component2
|
||||
@@ -41,7 +40,6 @@ import kotlin.collections.set
|
||||
import kotlin.properties.Delegates.observable
|
||||
|
||||
object SnodeAPI {
|
||||
private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) }
|
||||
internal val database: LokiAPIDatabaseProtocol
|
||||
get() = SnodeModule.shared.storage
|
||||
private val broadcaster: Broadcaster
|
||||
@@ -56,6 +54,11 @@ object SnodeAPI {
|
||||
* user's clock is incorrect.
|
||||
*/
|
||||
internal var clockOffset = 0L
|
||||
|
||||
@JvmStatic
|
||||
public val nowWithOffset
|
||||
get() = System.currentTimeMillis() + clockOffset
|
||||
|
||||
internal var forkInfo by observable(database.getForkInfo()) { _, oldValue, newValue ->
|
||||
if (newValue > oldValue) {
|
||||
Log.d("Loki", "Setting new fork info new: $newValue, old: $oldValue")
|
||||
@@ -68,12 +71,16 @@ object SnodeAPI {
|
||||
private val minimumSnodePoolCount = 12
|
||||
private val minimumSwarmSnodeCount = 3
|
||||
// Use port 4433 if the API level can handle the network security configuration and enforce pinned certificates
|
||||
private val seedNodePort = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) 443 else 4433
|
||||
private val seedNodePort = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) 443 else 4443
|
||||
private val seedNodePool by lazy {
|
||||
if (useTestnet) {
|
||||
setOf( "http://public.loki.foundation:38157" )
|
||||
} else {
|
||||
setOf( "https://storage.seed1.loki.network:$seedNodePort", "https://storage.seed3.loki.network:$seedNodePort", "https://public.loki.foundation:$seedNodePort" )
|
||||
setOf(
|
||||
"https://seed1.getsession.org:$seedNodePort",
|
||||
"https://seed2.getsession.org:$seedNodePort",
|
||||
"https://seed3.getsession.org:$seedNodePort",
|
||||
)
|
||||
}
|
||||
}
|
||||
private const val snodeFailureThreshold = 3
|
||||
@@ -93,6 +100,14 @@ object SnodeAPI {
|
||||
object ValidationFailed : Error("ONS name validation failed.")
|
||||
}
|
||||
|
||||
// Batch
|
||||
data class SnodeBatchRequestInfo(
|
||||
val method: String,
|
||||
val params: Map<String, Any>,
|
||||
@Transient
|
||||
val namespace: Int?
|
||||
) // assume signatures, pubkey and namespaces are attached in parameters if required
|
||||
|
||||
// Internal API
|
||||
internal fun invoke(
|
||||
method: Snode.Method,
|
||||
@@ -310,26 +325,32 @@ object SnodeAPI {
|
||||
fun getRawMessages(snode: Snode, publicKey: String, requiresAuth: Boolean = true, namespace: Int = 0): RawResponsePromise {
|
||||
// Get last message hash
|
||||
val lastHashValue = database.getLastMessageHashValue(snode, publicKey, namespace) ?: ""
|
||||
val parameters = mutableMapOf<String,Any>(
|
||||
val parameters = mutableMapOf<String, Any>(
|
||||
"pubKey" to publicKey,
|
||||
"last_hash" to lastHashValue,
|
||||
)
|
||||
// Construct signature
|
||||
if (requiresAuth) {
|
||||
val userED25519KeyPair = try {
|
||||
MessagingModuleConfiguration.shared.getUserED25519KeyPair() ?: return Promise.ofFail(Error.NoKeyPair)
|
||||
MessagingModuleConfiguration.shared.getUserED25519KeyPair()
|
||||
?: return Promise.ofFail(Error.NoKeyPair)
|
||||
} catch (e: Exception) {
|
||||
Log.e("Loki", "Error getting KeyPair", e)
|
||||
return Promise.ofFail(Error.NoKeyPair)
|
||||
}
|
||||
val timestamp = Date().time + SnodeAPI.clockOffset
|
||||
val timestamp = System.currentTimeMillis() + clockOffset
|
||||
val ed25519PublicKey = userED25519KeyPair.publicKey.asHexString
|
||||
val signature = ByteArray(Sign.BYTES)
|
||||
val verificationData =
|
||||
if (namespace != 0) "retrieve$namespace$timestamp".toByteArray()
|
||||
else "retrieve$timestamp".toByteArray()
|
||||
try {
|
||||
sodium.cryptoSignDetached(signature, verificationData, verificationData.size.toLong(), userED25519KeyPair.secretKey.asBytes)
|
||||
sodium.cryptoSignDetached(
|
||||
signature,
|
||||
verificationData,
|
||||
verificationData.size.toLong(),
|
||||
userED25519KeyPair.secretKey.asBytes
|
||||
)
|
||||
} catch (exception: Exception) {
|
||||
return Promise.ofFail(Error.SigningFailed)
|
||||
}
|
||||
@@ -345,7 +366,252 @@ object SnodeAPI {
|
||||
}
|
||||
|
||||
// Make the request
|
||||
return invoke(Snode.Method.GetMessages, snode, parameters, publicKey)
|
||||
return invoke(Snode.Method.Retrieve, snode, parameters, publicKey)
|
||||
}
|
||||
|
||||
fun buildAuthenticatedStoreBatchInfo(publicKey: String, namespace: Int, message: SnodeMessage): SnodeBatchRequestInfo? {
|
||||
val params = mutableMapOf<String, Any>()
|
||||
// load the message data params into the sub request
|
||||
// currently loads:
|
||||
// pubKey
|
||||
// data
|
||||
// ttl
|
||||
// timestamp
|
||||
params.putAll(message.toJSON())
|
||||
params["namespace"] = namespace
|
||||
|
||||
// used for sig generation since it is also the value used in timestamp parameter
|
||||
val messageTimestamp = message.timestamp
|
||||
|
||||
val userEd25519KeyPair = try {
|
||||
MessagingModuleConfiguration.shared.getUserED25519KeyPair() ?: return null
|
||||
} catch (e: Exception) {
|
||||
return null
|
||||
}
|
||||
|
||||
val ed25519PublicKey = userEd25519KeyPair.publicKey.asHexString
|
||||
val signature = ByteArray(Sign.BYTES)
|
||||
val verificationData = "store$namespace$messageTimestamp".toByteArray()
|
||||
try {
|
||||
sodium.cryptoSignDetached(
|
||||
signature,
|
||||
verificationData,
|
||||
verificationData.size.toLong(),
|
||||
userEd25519KeyPair.secretKey.asBytes
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e("Loki", "Signing data failed with user secret key", e)
|
||||
}
|
||||
// timestamp already set
|
||||
params["pubkey_ed25519"] = ed25519PublicKey
|
||||
params["signature"] = Base64.encodeBytes(signature)
|
||||
return SnodeBatchRequestInfo(
|
||||
Snode.Method.SendMessage.rawValue,
|
||||
params,
|
||||
namespace
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Message hashes can be shared across multiple namespaces (for a single public key destination)
|
||||
* @param publicKey the destination's identity public key to delete from (05...)
|
||||
* @param messageHashes a list of stored message hashes to delete from the server
|
||||
* @param required indicates that *at least one* message in the list is deleted from the server, otherwise it will return 404
|
||||
*/
|
||||
fun buildAuthenticatedDeleteBatchInfo(publicKey: String, messageHashes: List<String>, required: Boolean = false): SnodeBatchRequestInfo? {
|
||||
val params = mutableMapOf(
|
||||
"pubkey" to publicKey,
|
||||
"required" to required, // could be omitted technically but explicit here
|
||||
"messages" to messageHashes
|
||||
)
|
||||
val userEd25519KeyPair = try {
|
||||
MessagingModuleConfiguration.shared.getUserED25519KeyPair() ?: return null
|
||||
} catch (e: Exception) {
|
||||
return null
|
||||
}
|
||||
val ed25519PublicKey = userEd25519KeyPair.publicKey.asHexString
|
||||
val signature = ByteArray(Sign.BYTES)
|
||||
val verificationData = "delete${messageHashes.joinToString("")}".toByteArray()
|
||||
try {
|
||||
sodium.cryptoSignDetached(
|
||||
signature,
|
||||
verificationData,
|
||||
verificationData.size.toLong(),
|
||||
userEd25519KeyPair.secretKey.asBytes
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e("Loki", "Signing data failed with user secret key", e)
|
||||
return null
|
||||
}
|
||||
params["pubkey_ed25519"] = ed25519PublicKey
|
||||
params["signature"] = Base64.encodeBytes(signature)
|
||||
return SnodeBatchRequestInfo(
|
||||
Snode.Method.DeleteMessage.rawValue,
|
||||
params,
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
fun buildAuthenticatedRetrieveBatchRequest(snode: Snode, publicKey: String, namespace: Int = 0, maxSize: Int? = null): SnodeBatchRequestInfo? {
|
||||
val lastHashValue = database.getLastMessageHashValue(snode, publicKey, namespace) ?: ""
|
||||
val params = mutableMapOf<String, Any>(
|
||||
"pubkey" to publicKey,
|
||||
"last_hash" to lastHashValue,
|
||||
)
|
||||
val userEd25519KeyPair = try {
|
||||
MessagingModuleConfiguration.shared.getUserED25519KeyPair() ?: return null
|
||||
} catch (e: Exception) {
|
||||
return null
|
||||
}
|
||||
val ed25519PublicKey = userEd25519KeyPair.publicKey.asHexString
|
||||
val timestamp = System.currentTimeMillis() + clockOffset
|
||||
val signature = ByteArray(Sign.BYTES)
|
||||
val verificationData = if (namespace == 0) "retrieve$timestamp".toByteArray()
|
||||
else "retrieve$namespace$timestamp".toByteArray()
|
||||
try {
|
||||
sodium.cryptoSignDetached(
|
||||
signature,
|
||||
verificationData,
|
||||
verificationData.size.toLong(),
|
||||
userEd25519KeyPair.secretKey.asBytes
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e("Loki", "Signing data failed with user secret key", e)
|
||||
return null
|
||||
}
|
||||
params["timestamp"] = timestamp
|
||||
params["pubkey_ed25519"] = ed25519PublicKey
|
||||
params["signature"] = Base64.encodeBytes(signature)
|
||||
if (namespace != 0) {
|
||||
params["namespace"] = namespace
|
||||
}
|
||||
if (maxSize != null) {
|
||||
params["max_size"] = maxSize
|
||||
}
|
||||
return SnodeBatchRequestInfo(
|
||||
Snode.Method.Retrieve.rawValue,
|
||||
params,
|
||||
namespace
|
||||
)
|
||||
}
|
||||
|
||||
fun buildAuthenticatedAlterTtlBatchRequest(
|
||||
messageHashes: List<String>,
|
||||
newExpiry: Long,
|
||||
publicKey: String,
|
||||
shorten: Boolean = false,
|
||||
extend: Boolean = false): SnodeBatchRequestInfo? {
|
||||
val params = buildAlterTtlParams(messageHashes, newExpiry, publicKey, extend, shorten) ?: return null
|
||||
return SnodeBatchRequestInfo(
|
||||
Snode.Method.Expire.rawValue,
|
||||
params,
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
fun getRawBatchResponse(snode: Snode, publicKey: String, requests: List<SnodeBatchRequestInfo>, sequence: Boolean = false): RawResponsePromise {
|
||||
val parameters = mutableMapOf<String, Any>(
|
||||
"requests" to requests
|
||||
)
|
||||
return invoke(if (sequence) Snode.Method.Sequence else Snode.Method.Batch, snode, parameters, publicKey).success { rawResponses ->
|
||||
val responseList = (rawResponses["results"] as List<RawResponse>)
|
||||
responseList.forEachIndexed { index, response ->
|
||||
if (response["code"] as? Int != 200) {
|
||||
Log.w("Loki", "response code was not 200")
|
||||
handleSnodeError(
|
||||
response["code"] as? Int ?: 0,
|
||||
response,
|
||||
snode,
|
||||
publicKey
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getExpiries(messageHashes: List<String>, publicKey: String) : RawResponsePromise {
|
||||
val userEd25519KeyPair = MessagingModuleConfiguration.shared.getUserED25519KeyPair() ?: return Promise.ofFail(NullPointerException("No user key pair"))
|
||||
val hashes = messageHashes.takeIf { it.size != 1 } ?: (messageHashes + "///////////////////////////////////////////") // TODO remove this when bug is fixed on nodes.
|
||||
return retryIfNeeded(maxRetryCount) {
|
||||
val timestamp = System.currentTimeMillis() + clockOffset
|
||||
val signData = "${Snode.Method.GetExpiries.rawValue}$timestamp${hashes.joinToString(separator = "")}".toByteArray()
|
||||
|
||||
val ed25519PublicKey = userEd25519KeyPair.publicKey.asHexString
|
||||
val signature = ByteArray(Sign.BYTES)
|
||||
try {
|
||||
sodium.cryptoSignDetached(
|
||||
signature,
|
||||
signData,
|
||||
signData.size.toLong(),
|
||||
userEd25519KeyPair.secretKey.asBytes
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e("Loki", "Signing data failed with user secret key", e)
|
||||
return@retryIfNeeded Promise.ofFail(e)
|
||||
}
|
||||
val params = mapOf(
|
||||
"pubkey" to publicKey,
|
||||
"messages" to hashes,
|
||||
"timestamp" to timestamp,
|
||||
"pubkey_ed25519" to ed25519PublicKey,
|
||||
"signature" to Base64.encodeBytes(signature)
|
||||
)
|
||||
getSingleTargetSnode(publicKey) bind { snode ->
|
||||
invoke(Snode.Method.GetExpiries, snode, params, publicKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun alterTtl(messageHashes: List<String>, newExpiry: Long, publicKey: String, extend: Boolean = false, shorten: Boolean = false): RawResponsePromise {
|
||||
return retryIfNeeded(maxRetryCount) {
|
||||
val params = buildAlterTtlParams(messageHashes, newExpiry, publicKey, extend, shorten)
|
||||
?: return@retryIfNeeded Promise.ofFail(
|
||||
Exception("Couldn't build signed params for alterTtl request for newExpiry=$newExpiry, extend=$extend, shorten=$shorten")
|
||||
)
|
||||
getSingleTargetSnode(publicKey).bind { snode ->
|
||||
invoke(Snode.Method.Expire, snode, params, publicKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildAlterTtlParams( // TODO: in future this will probably need to use the closed group subkeys / admin keys for group swarms
|
||||
messageHashes: List<String>,
|
||||
newExpiry: Long,
|
||||
publicKey: String,
|
||||
extend: Boolean = false,
|
||||
shorten: Boolean = false): Map<String, Any>? {
|
||||
val userEd25519KeyPair = MessagingModuleConfiguration.shared.getUserED25519KeyPair() ?: return null
|
||||
val params = mutableMapOf(
|
||||
"expiry" to newExpiry,
|
||||
"messages" to messageHashes,
|
||||
)
|
||||
if (extend) {
|
||||
params["extend"] = true
|
||||
} else if (shorten) {
|
||||
params["shorten"] = true
|
||||
}
|
||||
val shortenOrExtend = if (extend) "extend" else if (shorten) "shorten" else ""
|
||||
|
||||
val signData = "${Snode.Method.Expire.rawValue}$shortenOrExtend$newExpiry${messageHashes.joinToString(separator = "")}".toByteArray()
|
||||
|
||||
val ed25519PublicKey = userEd25519KeyPair.publicKey.asHexString
|
||||
val signature = ByteArray(Sign.BYTES)
|
||||
try {
|
||||
sodium.cryptoSignDetached(
|
||||
signature,
|
||||
signData,
|
||||
signData.size.toLong(),
|
||||
userEd25519KeyPair.secretKey.asBytes
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e("Loki", "Signing data failed with user secret key", e)
|
||||
return null
|
||||
}
|
||||
params["pubkey"] = publicKey
|
||||
params["pubkey_ed25519"] = ed25519PublicKey
|
||||
params["signature"] = Base64.encodeBytes(signature)
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
fun getMessages(publicKey: String): MessageListPromise {
|
||||
@@ -371,7 +637,7 @@ object SnodeAPI {
|
||||
val parameters = message.toJSON().toMutableMap<String,Any>()
|
||||
// Construct signature
|
||||
if (requiresAuth) {
|
||||
val sigTimestamp = System.currentTimeMillis() + SnodeAPI.clockOffset
|
||||
val sigTimestamp = nowWithOffset
|
||||
val ed25519PublicKey = userED25519KeyPair.publicKey.asHexString
|
||||
val signature = ByteArray(Sign.BYTES)
|
||||
// assume namespace here is non-zero, as zero namespace doesn't require auth
|
||||
@@ -474,13 +740,14 @@ object SnodeAPI {
|
||||
retryIfNeeded(maxRetryCount) {
|
||||
getNetworkTime(snode).bind { (_, timestamp) ->
|
||||
val signature = ByteArray(Sign.BYTES)
|
||||
val verificationData = (Snode.Method.DeleteAll.rawValue + timestamp.toString()).toByteArray()
|
||||
val verificationData = (Snode.Method.DeleteAll.rawValue + Namespace.ALL + timestamp.toString()).toByteArray()
|
||||
sodium.cryptoSignDetached(signature, verificationData, verificationData.size.toLong(), userED25519KeyPair.secretKey.asBytes)
|
||||
val deleteMessageParams = mapOf(
|
||||
"pubkey" to userPublicKey,
|
||||
"pubkey_ed25519" to userED25519KeyPair.publicKey.asHexString,
|
||||
"timestamp" to timestamp,
|
||||
"signature" to Base64.encodeBytes(signature)
|
||||
"signature" to Base64.encodeBytes(signature),
|
||||
"namespace" to Namespace.ALL,
|
||||
)
|
||||
invoke(Snode.Method.DeleteAll, snode, deleteMessageParams, userPublicKey).map {
|
||||
rawResponse -> parseDeletions(userPublicKey, timestamp, rawResponse)
|
||||
@@ -493,11 +760,69 @@ object SnodeAPI {
|
||||
}
|
||||
}
|
||||
|
||||
fun parseRawMessagesResponse(rawResponse: RawResponse, snode: Snode, publicKey: String, namespace: Int = 0): List<Pair<SignalServiceProtos.Envelope, String?>> {
|
||||
fun updateExpiry(updatedExpiryMs: Long, serverHashes: List<String>): Promise<Map<String, Pair<List<String>, Long>>, Exception> {
|
||||
return retryIfNeeded(maxRetryCount) {
|
||||
val module = MessagingModuleConfiguration.shared
|
||||
val userED25519KeyPair = module.getUserED25519KeyPair() ?: return@retryIfNeeded Promise.ofFail(Error.NoKeyPair)
|
||||
val userPublicKey = module.storage.getUserPublicKey() ?: return@retryIfNeeded Promise.ofFail(Error.NoKeyPair)
|
||||
val updatedExpiryMsWithNetworkOffset = updatedExpiryMs + clockOffset
|
||||
getSingleTargetSnode(userPublicKey).bind { snode ->
|
||||
retryIfNeeded(maxRetryCount) {
|
||||
// "expire" || expiry || messages[0] || ... || messages[N]
|
||||
val verificationData =
|
||||
(Snode.Method.Expire.rawValue + updatedExpiryMsWithNetworkOffset + serverHashes.fold("") { a, v -> a + v }).toByteArray()
|
||||
val signature = ByteArray(Sign.BYTES)
|
||||
sodium.cryptoSignDetached(
|
||||
signature,
|
||||
verificationData,
|
||||
verificationData.size.toLong(),
|
||||
userED25519KeyPair.secretKey.asBytes
|
||||
)
|
||||
val params = mapOf(
|
||||
"pubkey" to userPublicKey,
|
||||
"pubkey_ed25519" to userED25519KeyPair.publicKey.asHexString,
|
||||
"expiry" to updatedExpiryMs,
|
||||
"messages" to serverHashes,
|
||||
"signature" to Base64.encodeBytes(signature)
|
||||
)
|
||||
invoke(Snode.Method.Expire, snode, params, userPublicKey).map { rawResponse ->
|
||||
val swarms = rawResponse["swarm"] as? Map<String, Any> ?: return@map mapOf()
|
||||
val result = swarms.mapNotNull { (hexSnodePublicKey, rawJSON) ->
|
||||
val json = rawJSON as? Map<String, Any> ?: return@mapNotNull null
|
||||
val isFailed = json["failed"] as? Boolean ?: false
|
||||
val statusCode = json["code"] as? String
|
||||
val reason = json["reason"] as? String
|
||||
hexSnodePublicKey to if (isFailed) {
|
||||
Log.e("Loki", "Failed to update expiry for: $hexSnodePublicKey due to error: $reason ($statusCode).")
|
||||
listOf<String>() to 0L
|
||||
} else {
|
||||
val hashes = json["updated"] as List<String>
|
||||
val expiryApplied = json["expiry"] as Long
|
||||
val signature = json["signature"] as String
|
||||
val snodePublicKey = Key.fromHexString(hexSnodePublicKey)
|
||||
// The signature looks like ( PUBKEY_HEX || RMSG[0] || ... || RMSG[N] || DMSG[0] || ... || DMSG[M] )
|
||||
val message = (userPublicKey + serverHashes.fold("") { a, v -> a + v } + hashes.fold("") { a, v -> a + v }).toByteArray()
|
||||
if (sodium.cryptoSignVerifyDetached(Base64.decode(signature), message, message.size, snodePublicKey.asBytes)) {
|
||||
hashes to expiryApplied
|
||||
} else listOf<String>() to 0L
|
||||
}
|
||||
}
|
||||
return@map result.toMap()
|
||||
}.fail { e ->
|
||||
Log.e("Loki", "Failed to update expiry", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun parseRawMessagesResponse(rawResponse: RawResponse, snode: Snode, publicKey: String, namespace: Int = 0, updateLatestHash: Boolean = true, updateStoredHashes: Boolean = true): List<Pair<SignalServiceProtos.Envelope, String?>> {
|
||||
val messages = rawResponse["messages"] as? List<*>
|
||||
return if (messages != null) {
|
||||
updateLastMessageHashValueIfPossible(snode, publicKey, messages, namespace)
|
||||
val newRawMessages = removeDuplicates(publicKey, messages, namespace)
|
||||
if (updateLatestHash) {
|
||||
updateLastMessageHashValueIfPossible(snode, publicKey, messages, namespace)
|
||||
}
|
||||
val newRawMessages = removeDuplicates(publicKey, messages, namespace, updateStoredHashes)
|
||||
return parseEnvelopes(newRawMessages)
|
||||
} else {
|
||||
listOf()
|
||||
@@ -514,7 +839,7 @@ object SnodeAPI {
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeDuplicates(publicKey: String, rawMessages: List<*>, namespace: Int): List<*> {
|
||||
private fun removeDuplicates(publicKey: String, rawMessages: List<*>, namespace: Int, updateStoredHashes: Boolean): List<*> {
|
||||
val originalMessageHashValues = database.getReceivedMessageHashValues(publicKey, namespace)?.toMutableSet() ?: mutableSetOf()
|
||||
val receivedMessageHashValues = originalMessageHashValues.toMutableSet()
|
||||
val result = rawMessages.filter { rawMessage ->
|
||||
@@ -529,7 +854,7 @@ object SnodeAPI {
|
||||
false
|
||||
}
|
||||
}
|
||||
if (originalMessageHashValues != receivedMessageHashValues) {
|
||||
if (originalMessageHashValues != receivedMessageHashValues && updateStoredHashes) {
|
||||
database.setReceivedMessageHashValues(publicKey, receivedMessageHashValues, namespace)
|
||||
}
|
||||
return result
|
||||
@@ -566,11 +891,11 @@ object SnodeAPI {
|
||||
Log.e("Loki", "Failed to delete all messages from: $hexSnodePublicKey due to error: $reason ($statusCode).")
|
||||
false
|
||||
} else {
|
||||
val hashes = json["deleted"] as List<String> // Hashes of deleted messages
|
||||
val hashes = (json["deleted"] as Map<String,List<String>>).flatMap { (_, hashes) -> hashes }.sorted() // Hashes of deleted messages
|
||||
val signature = json["signature"] as String
|
||||
val snodePublicKey = Key.fromHexString(hexSnodePublicKey)
|
||||
// The signature looks like ( PUBKEY_HEX || TIMESTAMP || DELETEDHASH[0] || ... || DELETEDHASH[N] )
|
||||
val message = (userPublicKey + timestamp.toString() + hashes.fold("") { a, v -> a + v }).toByteArray()
|
||||
val message = (userPublicKey + timestamp.toString() + hashes.joinToString(separator = "")).toByteArray()
|
||||
sodium.cryptoSignVerifyDetached(Base64.decode(signature), message, message.size, snodePublicKey.asBytes)
|
||||
}
|
||||
}
|
||||
@@ -626,6 +951,10 @@ object SnodeAPI {
|
||||
Log.d("Loki", "Got a 421 without an associated public key.")
|
||||
}
|
||||
}
|
||||
404 -> {
|
||||
Log.d("Loki", "404, probably no file found")
|
||||
return Error.Generic
|
||||
}
|
||||
else -> {
|
||||
handleBadSnode()
|
||||
Log.d("Loki", "Unhandled response code: ${statusCode}.")
|
||||
|
@@ -20,6 +20,7 @@ data class SnodeMessage(
|
||||
*/
|
||||
val timestamp: Long
|
||||
) {
|
||||
internal constructor(): this("", "", -1, -1)
|
||||
|
||||
internal fun toJSON(): Map<String, String> {
|
||||
return mapOf(
|
||||
|
@@ -1,6 +1,7 @@
|
||||
package org.session.libsession.utilities
|
||||
|
||||
import androidx.annotation.WorkerThread
|
||||
import org.session.libsignal.crypto.CipherUtil.CIPHER_LOCK
|
||||
import org.session.libsignal.utilities.ByteUtil
|
||||
import org.session.libsignal.utilities.Util
|
||||
import org.session.libsignal.utilities.Hex
|
||||
@@ -27,9 +28,11 @@ internal object AESGCM {
|
||||
internal fun decrypt(ivAndCiphertext: ByteArray, symmetricKey: ByteArray): ByteArray {
|
||||
val iv = ivAndCiphertext.sliceArray(0 until ivSize)
|
||||
val ciphertext = ivAndCiphertext.sliceArray(ivSize until ivAndCiphertext.count())
|
||||
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
|
||||
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(symmetricKey, "AES"), GCMParameterSpec(gcmTagSize, iv))
|
||||
return cipher.doFinal(ciphertext)
|
||||
synchronized(CIPHER_LOCK) {
|
||||
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
|
||||
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(symmetricKey, "AES"), GCMParameterSpec(gcmTagSize, iv))
|
||||
return cipher.doFinal(ciphertext)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -47,9 +50,11 @@ internal object AESGCM {
|
||||
*/
|
||||
internal fun encrypt(plaintext: ByteArray, symmetricKey: ByteArray): ByteArray {
|
||||
val iv = Util.getSecretBytes(ivSize)
|
||||
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
|
||||
cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(symmetricKey, "AES"), GCMParameterSpec(gcmTagSize, iv))
|
||||
return ByteUtil.combine(iv, cipher.doFinal(plaintext))
|
||||
synchronized(CIPHER_LOCK) {
|
||||
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
|
||||
cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(symmetricKey, "AES"), GCMParameterSpec(gcmTagSize, iv))
|
||||
return ByteUtil.combine(iv, cipher.doFinal(plaintext))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -5,11 +5,10 @@ import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import android.util.Pair
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import org.session.libsession.utilities.DelimiterUtil
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsignal.utilities.guava.Optional
|
||||
import org.session.libsignal.utilities.IdPrefix
|
||||
import org.session.libsignal.utilities.Util
|
||||
import java.util.*
|
||||
import org.session.libsignal.utilities.guava.Optional
|
||||
import java.util.LinkedList
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
import java.util.regex.Matcher
|
||||
import java.util.regex.Pattern
|
||||
@@ -23,15 +22,17 @@ class Address private constructor(address: String) : Parcelable, Comparable<Addr
|
||||
get() = GroupUtil.isEncodedGroup(address)
|
||||
val isClosedGroup: Boolean
|
||||
get() = GroupUtil.isClosedGroup(address)
|
||||
val isOpenGroup: Boolean
|
||||
get() = GroupUtil.isOpenGroup(address)
|
||||
val isOpenGroupInbox: Boolean
|
||||
get() = GroupUtil.isOpenGroupInbox(address)
|
||||
val isCommunity: Boolean
|
||||
get() = GroupUtil.isCommunity(address)
|
||||
val isCommunityInbox: Boolean
|
||||
get() = GroupUtil.isCommunityInbox(address)
|
||||
val isCommunityOutbox: Boolean
|
||||
get() = address.startsWith(IdPrefix.BLINDED.value) || address.startsWith(IdPrefix.BLINDEDV2.value)
|
||||
val isContact: Boolean
|
||||
get() = !(isGroup || isOpenGroupInbox)
|
||||
get() = !(isGroup || isCommunityInbox)
|
||||
|
||||
fun contactIdentifier(): String {
|
||||
if (!isContact && !isOpenGroup) {
|
||||
if (!isContact && !isCommunity) {
|
||||
if (isGroup) throw AssertionError("Not e164, is group")
|
||||
throw AssertionError("Not e164, unknown")
|
||||
}
|
||||
@@ -166,8 +167,9 @@ class Address private constructor(address: String) : Parcelable, Comparable<Addr
|
||||
@JvmStatic
|
||||
fun fromSerializedList(serialized: String, delimiter: Char): List<Address> {
|
||||
val escapedAddresses = DelimiterUtil.split(serialized, delimiter)
|
||||
val set = escapedAddresses.toSet().sorted()
|
||||
val addresses: MutableList<Address> = LinkedList()
|
||||
for (escapedAddress in escapedAddresses) {
|
||||
for (escapedAddress in set) {
|
||||
addresses.add(fromSerialized(DelimiterUtil.unescape(escapedAddress, delimiter)))
|
||||
}
|
||||
return addresses
|
||||
@@ -175,9 +177,9 @@ class Address private constructor(address: String) : Parcelable, Comparable<Addr
|
||||
|
||||
@JvmStatic
|
||||
fun toSerializedList(addresses: List<Address>, delimiter: Char): String {
|
||||
Collections.sort(addresses)
|
||||
val set = addresses.toSet().sorted()
|
||||
val escapedAddresses: MutableList<String> = LinkedList()
|
||||
for (address in addresses) {
|
||||
for (address in set) {
|
||||
escapedAddresses.add(DelimiterUtil.escape(address.serialize(), delimiter))
|
||||
}
|
||||
return Util.join(escapedAddresses, delimiter.toString() + "")
|
||||
|
@@ -0,0 +1,23 @@
|
||||
package org.session.libsession.utilities
|
||||
|
||||
import network.loki.messenger.libsession_util.ConfigBase
|
||||
import network.loki.messenger.libsession_util.Contacts
|
||||
import network.loki.messenger.libsession_util.ConversationVolatileConfig
|
||||
import network.loki.messenger.libsession_util.UserGroupsConfig
|
||||
import network.loki.messenger.libsession_util.UserProfile
|
||||
|
||||
interface ConfigFactoryProtocol {
|
||||
val user: UserProfile?
|
||||
val contacts: Contacts?
|
||||
val convoVolatile: ConversationVolatileConfig?
|
||||
val userGroups: UserGroupsConfig?
|
||||
fun getUserConfigs(): List<ConfigBase>
|
||||
fun persist(forConfigObject: ConfigBase, timestamp: Long)
|
||||
|
||||
fun conversationInConfig(publicKey: String?, groupPublicKey: String?, openGroupId: String?, visibleOnly: Boolean): Boolean
|
||||
fun canPerformChange(variant: String, publicKey: String, changeTimestampMs: Long): Boolean
|
||||
}
|
||||
|
||||
interface ConfigFactoryUpdateListener {
|
||||
fun notifyUpdates(forConfigObject: ConfigBase, messageTimestamp: Long)
|
||||
}
|
@@ -0,0 +1,6 @@
|
||||
package org.session.libsession.utilities
|
||||
|
||||
enum class Device(val value: String, val service: String = value) {
|
||||
ANDROID("android", "firebase"),
|
||||
HUAWEI("huawei");
|
||||
}
|
@@ -2,8 +2,11 @@ package org.session.libsession.utilities
|
||||
|
||||
import okhttp3.HttpUrl
|
||||
import org.session.libsession.messaging.file_server.FileServerApi
|
||||
import org.session.libsignal.utilities.HTTP
|
||||
import org.session.libsignal.utilities.Log
|
||||
import java.io.*
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.OutputStream
|
||||
|
||||
object DownloadUtilities {
|
||||
|
||||
@@ -13,7 +16,7 @@ object DownloadUtilities {
|
||||
@JvmStatic
|
||||
fun downloadFile(destination: File, url: String) {
|
||||
val outputStream = FileOutputStream(destination) // Throws
|
||||
var remainingAttempts = 4
|
||||
var remainingAttempts = 2
|
||||
var exception: Exception? = null
|
||||
while (remainingAttempts > 0) {
|
||||
remainingAttempts -= 1
|
||||
@@ -40,7 +43,11 @@ object DownloadUtilities {
|
||||
outputStream.write(it)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("Loki", "Couldn't download attachment.", e)
|
||||
when (e) {
|
||||
// No need for the stack trace for HTTP errors
|
||||
is HTTP.HTTPRequestFailedException -> Log.e("Loki", "Couldn't download attachment due to error: ${e.message}")
|
||||
else -> Log.e("Loki", "Couldn't download attachment", e)
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
@@ -1,50 +0,0 @@
|
||||
package org.session.libsession.utilities;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.session.libsession.R;
|
||||
|
||||
public class ExpirationUtil {
|
||||
|
||||
public static String getExpirationDisplayValue(Context context, int expirationTime) {
|
||||
if (expirationTime <= 0) {
|
||||
return context.getString(R.string.expiration_off);
|
||||
} else if (expirationTime < TimeUnit.MINUTES.toSeconds(1)) {
|
||||
return context.getResources().getQuantityString(R.plurals.expiration_seconds, expirationTime, expirationTime);
|
||||
} else if (expirationTime < TimeUnit.HOURS.toSeconds(1)) {
|
||||
int minutes = expirationTime / (int)TimeUnit.MINUTES.toSeconds(1);
|
||||
return context.getResources().getQuantityString(R.plurals.expiration_minutes, minutes, minutes);
|
||||
} else if (expirationTime < TimeUnit.DAYS.toSeconds(1)) {
|
||||
int hours = expirationTime / (int)TimeUnit.HOURS.toSeconds(1);
|
||||
return context.getResources().getQuantityString(R.plurals.expiration_hours, hours, hours);
|
||||
} else if (expirationTime < TimeUnit.DAYS.toSeconds(7)) {
|
||||
int days = expirationTime / (int)TimeUnit.DAYS.toSeconds(1);
|
||||
return context.getResources().getQuantityString(R.plurals.expiration_days, days, days);
|
||||
} else {
|
||||
int weeks = expirationTime / (int)TimeUnit.DAYS.toSeconds(7);
|
||||
return context.getResources().getQuantityString(R.plurals.expiration_weeks, weeks, weeks);
|
||||
}
|
||||
}
|
||||
|
||||
public static String getExpirationAbbreviatedDisplayValue(Context context, int expirationTime) {
|
||||
if (expirationTime < TimeUnit.MINUTES.toSeconds(1)) {
|
||||
return context.getResources().getString(R.string.expiration_seconds_abbreviated, expirationTime);
|
||||
} else if (expirationTime < TimeUnit.HOURS.toSeconds(1)) {
|
||||
int minutes = expirationTime / (int)TimeUnit.MINUTES.toSeconds(1);
|
||||
return context.getResources().getString(R.string.expiration_minutes_abbreviated, minutes);
|
||||
} else if (expirationTime < TimeUnit.DAYS.toSeconds(1)) {
|
||||
int hours = expirationTime / (int)TimeUnit.HOURS.toSeconds(1);
|
||||
return context.getResources().getString(R.string.expiration_hours_abbreviated, hours);
|
||||
} else if (expirationTime < TimeUnit.DAYS.toSeconds(7)) {
|
||||
int days = expirationTime / (int)TimeUnit.DAYS.toSeconds(1);
|
||||
return context.getResources().getString(R.string.expiration_days_abbreviated, days);
|
||||
} else {
|
||||
int weeks = expirationTime / (int)TimeUnit.DAYS.toSeconds(7);
|
||||
return context.getResources().getString(R.string.expiration_weeks_abbreviated, weeks);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@@ -0,0 +1,57 @@
|
||||
package org.session.libsession.utilities
|
||||
|
||||
import android.content.Context
|
||||
import network.loki.messenger.libsession_util.util.ExpiryMode
|
||||
import org.session.libsession.R
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.time.Duration
|
||||
|
||||
fun Context.getExpirationTypeDisplayValue(sent: Boolean) = if (sent) getString(R.string.MessageRecord_state_sent) else getString(R.string.MessageRecord_state_read)
|
||||
|
||||
object ExpirationUtil {
|
||||
@JvmStatic
|
||||
fun getExpirationDisplayValue(context: Context, duration: Duration): String = getExpirationDisplayValue(context, duration.inWholeSeconds.toInt())
|
||||
|
||||
@JvmStatic
|
||||
fun getExpirationDisplayValue(context: Context, expirationTime: Int): String {
|
||||
return if (expirationTime <= 0) {
|
||||
context.getString(R.string.expiration_off)
|
||||
} else if (expirationTime < TimeUnit.MINUTES.toSeconds(1)) {
|
||||
context.resources.getQuantityString(
|
||||
R.plurals.expiration_seconds,
|
||||
expirationTime,
|
||||
expirationTime
|
||||
)
|
||||
} else if (expirationTime < TimeUnit.HOURS.toSeconds(1)) {
|
||||
val minutes = expirationTime / TimeUnit.MINUTES.toSeconds(1).toInt()
|
||||
context.resources.getQuantityString(R.plurals.expiration_minutes, minutes, minutes)
|
||||
} else if (expirationTime < TimeUnit.DAYS.toSeconds(1)) {
|
||||
val hours = expirationTime / TimeUnit.HOURS.toSeconds(1).toInt()
|
||||
context.resources.getQuantityString(R.plurals.expiration_hours, hours, hours)
|
||||
} else if (expirationTime < TimeUnit.DAYS.toSeconds(7)) {
|
||||
val days = expirationTime / TimeUnit.DAYS.toSeconds(1).toInt()
|
||||
context.resources.getQuantityString(R.plurals.expiration_days, days, days)
|
||||
} else {
|
||||
val weeks = expirationTime / TimeUnit.DAYS.toSeconds(7).toInt()
|
||||
context.resources.getQuantityString(R.plurals.expiration_weeks, weeks, weeks)
|
||||
}
|
||||
}
|
||||
|
||||
fun getExpirationAbbreviatedDisplayValue(context: Context, expirationTime: Long): String {
|
||||
return if (expirationTime < TimeUnit.MINUTES.toSeconds(1)) {
|
||||
context.resources.getString(R.string.expiration_seconds_abbreviated, expirationTime)
|
||||
} else if (expirationTime < TimeUnit.HOURS.toSeconds(1)) {
|
||||
val minutes = expirationTime / TimeUnit.MINUTES.toSeconds(1)
|
||||
context.resources.getString(R.string.expiration_minutes_abbreviated, minutes)
|
||||
} else if (expirationTime < TimeUnit.DAYS.toSeconds(1)) {
|
||||
val hours = expirationTime / TimeUnit.HOURS.toSeconds(1)
|
||||
context.resources.getString(R.string.expiration_hours_abbreviated, hours)
|
||||
} else if (expirationTime < TimeUnit.DAYS.toSeconds(7)) {
|
||||
val days = expirationTime / TimeUnit.DAYS.toSeconds(1)
|
||||
context.resources.getString(R.string.expiration_days_abbreviated, days)
|
||||
} else {
|
||||
val weeks = expirationTime / TimeUnit.DAYS.toSeconds(7)
|
||||
context.resources.getString(R.string.expiration_weeks_abbreviated, weeks)
|
||||
}
|
||||
}
|
||||
}
|
@@ -22,7 +22,7 @@ class GroupRecord(
|
||||
}
|
||||
|
||||
val isOpenGroup: Boolean
|
||||
get() = Address.fromSerialized(encodedId).isOpenGroup
|
||||
get() = Address.fromSerialized(encodedId).isCommunity
|
||||
val isClosedGroup: Boolean
|
||||
get() = Address.fromSerialized(encodedId).isClosedGroup
|
||||
|
||||
@@ -34,4 +34,4 @@ class GroupRecord(
|
||||
this.admins = Address.fromSerializedList(admins!!, ',')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,23 +1,31 @@
|
||||
package org.session.libsession.utilities
|
||||
|
||||
import org.session.libsession.messaging.open_groups.OpenGroup
|
||||
import org.session.libsession.messaging.utilities.SessionId
|
||||
import org.session.libsignal.messages.SignalServiceGroup
|
||||
import org.session.libsignal.utilities.Hex
|
||||
import java.io.IOException
|
||||
import kotlin.jvm.Throws
|
||||
|
||||
object GroupUtil {
|
||||
const val CLOSED_GROUP_PREFIX = "__textsecure_group__!"
|
||||
const val OPEN_GROUP_PREFIX = "__loki_public_chat_group__!"
|
||||
const val OPEN_GROUP_INBOX_PREFIX = "__open_group_inbox__!"
|
||||
const val COMMUNITY_PREFIX = "__loki_public_chat_group__!"
|
||||
const val COMMUNITY_INBOX_PREFIX = "__open_group_inbox__!"
|
||||
|
||||
@JvmStatic
|
||||
fun getEncodedOpenGroupID(groupID: ByteArray): String {
|
||||
return OPEN_GROUP_PREFIX + Hex.toStringCondensed(groupID)
|
||||
return COMMUNITY_PREFIX + Hex.toStringCondensed(groupID)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getEncodedOpenGroupInboxID(groupInboxID: ByteArray): String {
|
||||
return OPEN_GROUP_INBOX_PREFIX + Hex.toStringCondensed(groupInboxID)
|
||||
fun getEncodedOpenGroupInboxID(openGroup: OpenGroup, sessionId: SessionId): Address {
|
||||
val openGroupInboxId =
|
||||
"${openGroup.server}!${openGroup.publicKey}!${sessionId.hexString}".toByteArray()
|
||||
return getEncodedOpenGroupInboxID(openGroupInboxId)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getEncodedOpenGroupInboxID(groupInboxID: ByteArray): Address {
|
||||
return Address.fromSerialized(COMMUNITY_INBOX_PREFIX + Hex.toStringCondensed(groupInboxID))
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@@ -52,7 +60,7 @@ object GroupUtil {
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getDecodedOpenGroupInbox(groupID: String): String {
|
||||
fun getDecodedOpenGroupInboxSessionId(groupID: String): String {
|
||||
val decodedGroupId = getDecodedGroupID(groupID)
|
||||
if (decodedGroupId.split("!").count() > 2) {
|
||||
return decodedGroupId.split("!", limit = 3)[2]
|
||||
@@ -61,17 +69,17 @@ object GroupUtil {
|
||||
}
|
||||
|
||||
fun isEncodedGroup(groupId: String): Boolean {
|
||||
return groupId.startsWith(CLOSED_GROUP_PREFIX) || groupId.startsWith(OPEN_GROUP_PREFIX)
|
||||
return groupId.startsWith(CLOSED_GROUP_PREFIX) || groupId.startsWith(COMMUNITY_PREFIX)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun isOpenGroup(groupId: String): Boolean {
|
||||
return groupId.startsWith(OPEN_GROUP_PREFIX)
|
||||
fun isCommunity(groupId: String): Boolean {
|
||||
return groupId.startsWith(COMMUNITY_PREFIX)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun isOpenGroupInbox(groupId: String): Boolean {
|
||||
return groupId.startsWith(OPEN_GROUP_INBOX_PREFIX)
|
||||
fun isCommunityInbox(groupId: String): Boolean {
|
||||
return groupId.startsWith(COMMUNITY_INBOX_PREFIX)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@@ -97,4 +105,29 @@ object GroupUtil {
|
||||
fun doubleDecodeGroupID(groupID: String): ByteArray {
|
||||
return getDecodedGroupIDAsData(getDecodedGroupID(groupID))
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@Throws(IOException::class)
|
||||
fun doubleDecodeGroupId(groupID: String): String =
|
||||
Hex.toStringCondensed(getDecodedGroupIDAsData(getDecodedGroupID(groupID)))
|
||||
|
||||
@JvmStatic
|
||||
fun addressToGroupSessionId(address: Address): String =
|
||||
doubleDecodeGroupId(address.toGroupString())
|
||||
|
||||
fun createConfigMemberMap(
|
||||
members: Collection<String>,
|
||||
admins: Collection<String>
|
||||
): Map<String, Boolean> {
|
||||
// Start with admins
|
||||
val memberMap = admins.associateWith { true }.toMutableMap()
|
||||
|
||||
// Add the remaining members (there may be duplicates, so only add ones that aren't already in there from admins)
|
||||
for (member in members) {
|
||||
if (member !in memberMap) {
|
||||
memberMap[member] = false
|
||||
}
|
||||
}
|
||||
return memberMap
|
||||
}
|
||||
}
|
@@ -0,0 +1,4 @@
|
||||
package org.session.libsession.utilities
|
||||
|
||||
fun truncateIdForDisplay(id: String): String =
|
||||
id.takeIf { it.length > 8 }?.apply{ "${take(4)}…${takeLast(4)}" } ?: id
|
@@ -1,23 +1,24 @@
|
||||
package org.session.libsession.utilities;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.session.libsignal.utilities.Base64;
|
||||
import org.session.libsession.utilities.TextSecurePreferences;
|
||||
import org.session.libsession.utilities.Util;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class ProfileKeyUtil {
|
||||
|
||||
public static final int PROFILE_KEY_BYTES = 32;
|
||||
|
||||
public static synchronized @NonNull byte[] getProfileKey(@NonNull Context context) {
|
||||
try {
|
||||
String encodedProfileKey = TextSecurePreferences.getProfileKey(context);
|
||||
|
||||
if (encodedProfileKey == null) {
|
||||
encodedProfileKey = Util.getSecret(32);
|
||||
encodedProfileKey = Util.getSecret(PROFILE_KEY_BYTES);
|
||||
TextSecurePreferences.setProfileKey(context, encodedProfileKey);
|
||||
}
|
||||
|
||||
@@ -36,7 +37,7 @@ public class ProfileKeyUtil {
|
||||
}
|
||||
|
||||
public static synchronized @NonNull String generateEncodedProfileKey(@NonNull Context context) {
|
||||
return Util.getSecret(32);
|
||||
return Util.getSecret(PROFILE_KEY_BYTES);
|
||||
}
|
||||
|
||||
public static synchronized void setEncodedProfileKey(@NonNull Context context, @Nullable String key) {
|
||||
|
@@ -1,9 +1,15 @@
|
||||
package org.session.libsession.utilities
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import network.loki.messenger.libsession_util.util.ExpiryMode
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.contacts.Contact
|
||||
import org.session.libsession.messaging.messages.Message
|
||||
import org.session.libsession.messaging.messages.control.ClosedGroupControlMessage
|
||||
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate
|
||||
import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.snode.SnodeAPI.nowWithOffset
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
|
||||
class SSKEnvironment(
|
||||
@@ -26,20 +32,48 @@ class SSKEnvironment(
|
||||
|
||||
interface ProfileManagerProtocol {
|
||||
companion object {
|
||||
const val NAME_PADDED_LENGTH = 26
|
||||
const val NAME_PADDED_LENGTH = 64
|
||||
}
|
||||
|
||||
fun setNickname(context: Context, recipient: Recipient, nickname: String?)
|
||||
fun setName(context: Context, recipient: Recipient, name: String)
|
||||
fun setProfilePictureURL(context: Context, recipient: Recipient, profilePictureURL: String)
|
||||
fun setProfileKey(context: Context, recipient: Recipient, profileKey: ByteArray)
|
||||
fun setName(context: Context, recipient: Recipient, name: String?)
|
||||
fun setProfilePicture(context: Context, recipient: Recipient, profilePictureURL: String?, profileKey: ByteArray?)
|
||||
fun setUnidentifiedAccessMode(context: Context, recipient: Recipient, unidentifiedAccessMode: Recipient.UnidentifiedAccessMode)
|
||||
fun contactUpdatedInternal(contact: Contact): String?
|
||||
}
|
||||
|
||||
interface MessageExpirationManagerProtocol {
|
||||
fun setExpirationTimer(message: ExpirationTimerUpdate)
|
||||
fun disableExpirationTimer(message: ExpirationTimerUpdate)
|
||||
fun startAnyExpiration(timestamp: Long, author: String)
|
||||
fun insertExpirationTimerMessage(message: ExpirationTimerUpdate)
|
||||
fun startAnyExpiration(timestamp: Long, author: String, expireStartedAt: Long)
|
||||
|
||||
fun maybeStartExpiration(message: Message, startDisappearAfterRead: Boolean = false) {
|
||||
if (message is ExpirationTimerUpdate && message.isGroup || message is ClosedGroupControlMessage) return
|
||||
|
||||
maybeStartExpiration(
|
||||
message.sentTimestamp ?: return,
|
||||
message.sender ?: return,
|
||||
message.expiryMode,
|
||||
startDisappearAfterRead || message.isSenderSelf
|
||||
)
|
||||
}
|
||||
|
||||
fun startDisappearAfterRead(timestamp: Long, sender: String) {
|
||||
startAnyExpiration(
|
||||
timestamp,
|
||||
sender,
|
||||
expireStartedAt = nowWithOffset.coerceAtLeast(timestamp + 1)
|
||||
)
|
||||
}
|
||||
|
||||
fun maybeStartExpiration(timestamp: Long, sender: String, mode: ExpiryMode, startDisappearAfterRead: Boolean = false) {
|
||||
val expireStartedAt = when (mode) {
|
||||
is ExpiryMode.AfterSend -> timestamp
|
||||
is ExpiryMode.AfterRead -> if (startDisappearAfterRead) nowWithOffset.coerceAtLeast(timestamp + 1) else return
|
||||
else -> return
|
||||
}
|
||||
|
||||
startAnyExpiration(timestamp, sender, expireStartedAt)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -54,4 +88,4 @@ class SSKEnvironment(
|
||||
shared = SSKEnvironment(typingIndicators, readReceiptManager, profileManager, notificationManager, messageExpirationManager)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -12,7 +12,6 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import org.session.libsession.BuildConfig
|
||||
import org.session.libsession.R
|
||||
import org.session.libsession.utilities.TextSecurePreferences.Companion.AUTOPLAY_AUDIO_MESSAGES
|
||||
import org.session.libsession.utilities.TextSecurePreferences.Companion.CALL_NOTIFICATIONS_ENABLED
|
||||
@@ -38,12 +37,12 @@ interface TextSecurePreferences {
|
||||
fun setLastConfigurationSyncTime(value: Long)
|
||||
fun getConfigurationMessageSynced(): Boolean
|
||||
fun setConfigurationMessageSynced(value: Boolean)
|
||||
fun isUsingFCM(): Boolean
|
||||
fun setIsUsingFCM(value: Boolean)
|
||||
fun getFCMToken(): String?
|
||||
fun setFCMToken(value: String)
|
||||
fun getLastFCMUploadTime(): Long
|
||||
fun setLastFCMUploadTime(value: Long)
|
||||
fun isPushEnabled(): Boolean
|
||||
fun setPushEnabled(value: Boolean)
|
||||
fun getPushToken(): String?
|
||||
fun setPushToken(value: String)
|
||||
fun getPushRegisterTime(): Long
|
||||
fun setPushRegisterTime(value: Long)
|
||||
fun isScreenLockEnabled(): Boolean
|
||||
fun setScreenLockEnabled(value: Boolean)
|
||||
fun getScreenLockTimeout(): Long
|
||||
@@ -103,6 +102,8 @@ interface TextSecurePreferences {
|
||||
fun setUpdateApkDigest(value: String?)
|
||||
fun getUpdateApkDigest(): String?
|
||||
fun getLocalNumber(): String?
|
||||
fun getHasLegacyConfig(): Boolean
|
||||
fun setHasLegacyConfig(newValue: Boolean)
|
||||
fun setLocalNumber(localNumber: String)
|
||||
fun removeLocalNumber()
|
||||
fun isEnterSendsEnabled(): Boolean
|
||||
@@ -178,6 +179,7 @@ interface TextSecurePreferences {
|
||||
fun setThemeStyle(themeStyle: String)
|
||||
fun setFollowSystemSettings(followSystemSettings: Boolean)
|
||||
fun autoplayAudioMessages(): Boolean
|
||||
fun hasForcedNewConfig(): Boolean
|
||||
fun hasPreference(key: String): Boolean
|
||||
fun clearAll()
|
||||
|
||||
@@ -187,6 +189,9 @@ interface TextSecurePreferences {
|
||||
internal val _events = MutableSharedFlow<String>(0, 64, BufferOverflow.DROP_OLDEST)
|
||||
val events get() = _events.asSharedFlow()
|
||||
|
||||
@JvmStatic
|
||||
var pushSuffix = ""
|
||||
|
||||
const val DISABLE_PASSPHRASE_PREF = "pref_disable_passphrase"
|
||||
const val LANGUAGE_PREF = "pref_language"
|
||||
const val THREAD_TRIM_NOW = "pref_trim_now"
|
||||
@@ -249,9 +254,9 @@ interface TextSecurePreferences {
|
||||
const val LINK_PREVIEWS = "pref_link_previews"
|
||||
const val GIF_METADATA_WARNING = "has_seen_gif_metadata_warning"
|
||||
const val GIF_GRID_LAYOUT = "pref_gif_grid_layout"
|
||||
const val IS_USING_FCM = "pref_is_using_fcm"
|
||||
const val FCM_TOKEN = "pref_fcm_token"
|
||||
const val LAST_FCM_TOKEN_UPLOAD_TIME = "pref_last_fcm_token_upload_time_2"
|
||||
val IS_PUSH_ENABLED get() = "pref_is_using_fcm$pushSuffix"
|
||||
val PUSH_TOKEN get() = "pref_fcm_token_2$pushSuffix"
|
||||
val PUSH_REGISTER_TIME get() = "pref_last_fcm_token_upload_time_2$pushSuffix"
|
||||
const val LAST_CONFIGURATION_SYNC_TIME = "pref_last_configuration_sync_time"
|
||||
const val CONFIGURATION_SYNCED = "pref_configuration_synced"
|
||||
const val LAST_PROFILE_UPDATE_TIME = "pref_last_profile_update_time"
|
||||
@@ -264,6 +269,10 @@ interface TextSecurePreferences {
|
||||
const val AUTOPLAY_AUDIO_MESSAGES = "pref_autoplay_audio"
|
||||
const val FINGERPRINT_KEY_GENERATED = "fingerprint_key_generated"
|
||||
const val SELECTED_ACCENT_COLOR = "selected_accent_color"
|
||||
|
||||
const val HAS_RECEIVED_LEGACY_CONFIG = "has_received_legacy_config"
|
||||
const val HAS_FORCED_NEW_CONFIG = "has_forced_new_config"
|
||||
|
||||
const val GREEN_ACCENT = "accent_green"
|
||||
const val BLUE_ACCENT = "accent_blue"
|
||||
const val PURPLE_ACCENT = "accent_purple"
|
||||
@@ -281,6 +290,8 @@ interface TextSecurePreferences {
|
||||
const val OCEAN_DARK = "ocean.dark"
|
||||
const val OCEAN_LIGHT = "ocean.light"
|
||||
|
||||
const val ALLOW_MESSAGE_REQUESTS = "libsession.ALLOW_MESSAGE_REQUESTS"
|
||||
|
||||
@JvmStatic
|
||||
fun getLastConfigurationSyncTime(context: Context): Long {
|
||||
return getLongPreference(context, LAST_CONFIGURATION_SYNC_TIME, 0)
|
||||
@@ -303,31 +314,31 @@ interface TextSecurePreferences {
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun isUsingFCM(context: Context): Boolean {
|
||||
return getBooleanPreference(context, IS_USING_FCM, false)
|
||||
fun isPushEnabled(context: Context): Boolean {
|
||||
return getBooleanPreference(context, IS_PUSH_ENABLED, false)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun setIsUsingFCM(context: Context, value: Boolean) {
|
||||
setBooleanPreference(context, IS_USING_FCM, value)
|
||||
fun setPushEnabled(context: Context, value: Boolean) {
|
||||
setBooleanPreference(context, IS_PUSH_ENABLED, value)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getFCMToken(context: Context): String? {
|
||||
return getStringPreference(context, FCM_TOKEN, "")
|
||||
fun getPushToken(context: Context): String? {
|
||||
return getStringPreference(context, PUSH_TOKEN, "")
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun setFCMToken(context: Context, value: String) {
|
||||
setStringPreference(context, FCM_TOKEN, value)
|
||||
fun setPushToken(context: Context, value: String?) {
|
||||
setStringPreference(context, PUSH_TOKEN, value)
|
||||
}
|
||||
|
||||
fun getLastFCMUploadTime(context: Context): Long {
|
||||
return getLongPreference(context, LAST_FCM_TOKEN_UPLOAD_TIME, 0)
|
||||
fun getPushRegisterTime(context: Context): Long {
|
||||
return getLongPreference(context, PUSH_REGISTER_TIME, 0)
|
||||
}
|
||||
|
||||
fun setLastFCMUploadTime(context: Context, value: Long) {
|
||||
setLongPreference(context, LAST_FCM_TOKEN_UPLOAD_TIME, value)
|
||||
fun setPushRegisterTime(context: Context, value: Long) {
|
||||
setLongPreference(context, PUSH_REGISTER_TIME, value)
|
||||
}
|
||||
|
||||
// endregion
|
||||
@@ -625,6 +636,17 @@ interface TextSecurePreferences {
|
||||
return getStringPreference(context, LOCAL_NUMBER_PREF, null)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getHasLegacyConfig(context: Context): Boolean {
|
||||
return getBooleanPreference(context, HAS_RECEIVED_LEGACY_CONFIG, false)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun setHasLegacyConfig(context: Context, newValue: Boolean) {
|
||||
setBooleanPreference(context, HAS_RECEIVED_LEGACY_CONFIG, newValue)
|
||||
_events.tryEmit(HAS_RECEIVED_LEGACY_CONFIG)
|
||||
}
|
||||
|
||||
fun setLocalNumber(context: Context, localNumber: String) {
|
||||
setStringPreference(context, LOCAL_NUMBER_PREF, localNumber.toLowerCase())
|
||||
}
|
||||
@@ -649,7 +671,7 @@ interface TextSecurePreferences {
|
||||
|
||||
@JvmStatic
|
||||
fun isScreenSecurityEnabled(context: Context): Boolean {
|
||||
return getBooleanPreference(context, SCREEN_SECURITY_PREF, !BuildConfig.DEBUG)
|
||||
return getBooleanPreference(context, SCREEN_SECURITY_PREF, context.resources.getBoolean(R.bool.screen_security_default))
|
||||
}
|
||||
|
||||
fun getLastVersionCode(context: Context): Int {
|
||||
@@ -795,6 +817,11 @@ interface TextSecurePreferences {
|
||||
setIntegerPreference(context, NOTIFICATION_MESSAGES_CHANNEL_VERSION, version)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun hasForcedNewConfig(context: Context): Boolean {
|
||||
return getBooleanPreference(context, HAS_FORCED_NEW_CONFIG, false)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getBooleanPreference(context: Context, key: String?, defaultValue: Boolean): Boolean {
|
||||
return getDefaultSharedPreferences(context).getBoolean(key, defaultValue)
|
||||
@@ -815,7 +842,7 @@ interface TextSecurePreferences {
|
||||
getDefaultSharedPreferences(context).edit().putString(key, value).apply()
|
||||
}
|
||||
|
||||
private fun getIntegerPreference(context: Context, key: String, defaultValue: Int): Int {
|
||||
fun getIntegerPreference(context: Context, key: String, defaultValue: Int): Int {
|
||||
return getDefaultSharedPreferences(context).getInt(key, defaultValue)
|
||||
}
|
||||
|
||||
@@ -986,7 +1013,6 @@ interface TextSecurePreferences {
|
||||
fun clearAll(context: Context) {
|
||||
getDefaultSharedPreferences(context).edit().clear().commit()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1011,28 +1037,28 @@ class AppTextSecurePreferences @Inject constructor(
|
||||
TextSecurePreferences._events.tryEmit(TextSecurePreferences.CONFIGURATION_SYNCED)
|
||||
}
|
||||
|
||||
override fun isUsingFCM(): Boolean {
|
||||
return getBooleanPreference(TextSecurePreferences.IS_USING_FCM, false)
|
||||
override fun isPushEnabled(): Boolean {
|
||||
return getBooleanPreference(TextSecurePreferences.IS_PUSH_ENABLED, false)
|
||||
}
|
||||
|
||||
override fun setIsUsingFCM(value: Boolean) {
|
||||
setBooleanPreference(TextSecurePreferences.IS_USING_FCM, value)
|
||||
override fun setPushEnabled(value: Boolean) {
|
||||
setBooleanPreference(TextSecurePreferences.IS_PUSH_ENABLED, value)
|
||||
}
|
||||
|
||||
override fun getFCMToken(): String? {
|
||||
return getStringPreference(TextSecurePreferences.FCM_TOKEN, "")
|
||||
override fun getPushToken(): String? {
|
||||
return getStringPreference(TextSecurePreferences.PUSH_TOKEN, "")
|
||||
}
|
||||
|
||||
override fun setFCMToken(value: String) {
|
||||
setStringPreference(TextSecurePreferences.FCM_TOKEN, value)
|
||||
override fun setPushToken(value: String) {
|
||||
setStringPreference(TextSecurePreferences.PUSH_TOKEN, value)
|
||||
}
|
||||
|
||||
override fun getLastFCMUploadTime(): Long {
|
||||
return getLongPreference(TextSecurePreferences.LAST_FCM_TOKEN_UPLOAD_TIME, 0)
|
||||
override fun getPushRegisterTime(): Long {
|
||||
return getLongPreference(TextSecurePreferences.PUSH_REGISTER_TIME, 0)
|
||||
}
|
||||
|
||||
override fun setLastFCMUploadTime(value: Long) {
|
||||
setLongPreference(TextSecurePreferences.LAST_FCM_TOKEN_UPLOAD_TIME, value)
|
||||
override fun setPushRegisterTime(value: Long) {
|
||||
setLongPreference(TextSecurePreferences.PUSH_REGISTER_TIME, value)
|
||||
}
|
||||
|
||||
override fun isScreenLockEnabled(): Boolean {
|
||||
@@ -1279,6 +1305,15 @@ class AppTextSecurePreferences @Inject constructor(
|
||||
return getStringPreference(TextSecurePreferences.LOCAL_NUMBER_PREF, null)
|
||||
}
|
||||
|
||||
override fun getHasLegacyConfig(): Boolean {
|
||||
return getBooleanPreference(TextSecurePreferences.HAS_RECEIVED_LEGACY_CONFIG, false)
|
||||
}
|
||||
|
||||
override fun setHasLegacyConfig(newValue: Boolean) {
|
||||
setBooleanPreference(TextSecurePreferences.HAS_RECEIVED_LEGACY_CONFIG, newValue)
|
||||
TextSecurePreferences._events.tryEmit(TextSecurePreferences.HAS_RECEIVED_LEGACY_CONFIG)
|
||||
}
|
||||
|
||||
override fun setLocalNumber(localNumber: String) {
|
||||
setStringPreference(TextSecurePreferences.LOCAL_NUMBER_PREF, localNumber.toLowerCase())
|
||||
}
|
||||
@@ -1422,6 +1457,9 @@ class AppTextSecurePreferences @Inject constructor(
|
||||
setIntegerPreference(TextSecurePreferences.NOTIFICATION_MESSAGES_CHANNEL_VERSION, version)
|
||||
}
|
||||
|
||||
override fun hasForcedNewConfig(): Boolean =
|
||||
getBooleanPreference(TextSecurePreferences.HAS_FORCED_NEW_CONFIG, false)
|
||||
|
||||
override fun getBooleanPreference(key: String?, defaultValue: Boolean): Boolean {
|
||||
return getDefaultSharedPreferences(context).getBoolean(key, defaultValue)
|
||||
}
|
||||
|
@@ -27,6 +27,10 @@ public class ThemeUtil {
|
||||
return getAttributeText(context, R.attr.theme_type, "light").equals("dark");
|
||||
}
|
||||
|
||||
public static boolean isLightTheme(@NonNull Context context) {
|
||||
return getAttributeText(context, R.attr.theme_type, "light").equals("light");
|
||||
}
|
||||
|
||||
public static boolean getThemedBoolean(@NonNull Context context, @AttrRes int attr) {
|
||||
TypedValue typedValue = new TypedValue();
|
||||
Resources.Theme theme = context.getTheme();
|
||||
|
@@ -365,4 +365,21 @@ object Util {
|
||||
val digitGroups = (Math.log10(sizeBytes.toDouble()) / Math.log10(1024.0)).toInt()
|
||||
return DecimalFormat("#,##0.#").format(sizeBytes / Math.pow(1024.0, digitGroups.toDouble())) + " " + units[digitGroups]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun <T, R> T.runIf(condition: Boolean, block: T.() -> R): R where T: R = if (condition) block() else this
|
||||
|
||||
fun <T, K: Any> Iterable<T>.associateByNotNull(
|
||||
keySelector: (T) -> K?
|
||||
) = associateByNotNull(keySelector) { it }
|
||||
|
||||
fun <T, K: Any, V: Any> Iterable<T>.associateByNotNull(
|
||||
keySelector: (T) -> K?,
|
||||
valueTransform: (T) -> V?,
|
||||
): Map<K, V> = buildMap {
|
||||
for (item in this@associateByNotNull) {
|
||||
val key = keySelector(item) ?: continue
|
||||
val value = valueTransform(item) ?: continue
|
||||
this[key] = value
|
||||
}
|
||||
}
|
||||
|
@@ -2,6 +2,8 @@ package org.session.libsession.utilities
|
||||
|
||||
import android.content.Context
|
||||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.annotation.ColorInt
|
||||
|
||||
@@ -13,4 +15,8 @@ fun Context.getColorFromAttr(
|
||||
): Int {
|
||||
theme.resolveAttribute(attrColor, typedValue, resolveRefs)
|
||||
return typedValue.data
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <reified LP: ViewGroup.LayoutParams> View.modifyLayoutParams(function: LP.() -> Unit) {
|
||||
layoutParams = (layoutParams as LP).apply { function() }
|
||||
}
|
||||
|
@@ -0,0 +1,169 @@
|
||||
package org.session.libsession.utilities.bencode
|
||||
|
||||
import java.util.LinkedList
|
||||
|
||||
object Bencode {
|
||||
class Decoder(source: ByteArray) {
|
||||
|
||||
private val iterator = LinkedList<Byte>().apply {
|
||||
addAll(source.asIterable())
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode an element based on next marker assumed to be string/int/list/dict or return null
|
||||
*/
|
||||
fun decode(): BencodeElement? {
|
||||
val result = when (iterator.peek()?.toInt()?.toChar()) {
|
||||
in NUMBERS -> decodeString()
|
||||
INT_INDICATOR -> decodeInt()
|
||||
LIST_INDICATOR -> decodeList()
|
||||
DICT_INDICATOR -> decodeDict()
|
||||
else -> {
|
||||
null
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a string element from iterator assumed to have structure `{length}:{data}`
|
||||
*/
|
||||
private fun decodeString(): BencodeString? {
|
||||
val lengthStrings = buildString {
|
||||
while (iterator.isNotEmpty() && iterator.peek()?.toInt()?.toChar() != SEPARATOR) {
|
||||
append(iterator.pop().toInt().toChar())
|
||||
}
|
||||
}
|
||||
iterator.pop() // drop `:`
|
||||
val length = lengthStrings.toIntOrNull(10) ?: return null
|
||||
val remaining = (0 until length).map { iterator.pop() }.toByteArray()
|
||||
return BencodeString(remaining)
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode an int element from iterator assumed to have structure `i{int}e`
|
||||
*/
|
||||
private fun decodeInt(): BencodeElement? {
|
||||
iterator.pop() // drop `i`
|
||||
val intString = buildString {
|
||||
while (iterator.isNotEmpty() && iterator.peek()?.toInt()?.toChar() != END_INDICATOR) {
|
||||
append(iterator.pop().toInt().toChar())
|
||||
}
|
||||
}
|
||||
val asInt = intString.toIntOrNull(10) ?: return null
|
||||
iterator.pop() // drop `e`
|
||||
return BencodeInteger(asInt)
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a list element from iterator assumed to have structure `l{data}e`
|
||||
*/
|
||||
private fun decodeList(): BencodeElement {
|
||||
iterator.pop() // drop `l`
|
||||
val listElements = mutableListOf<BencodeElement>()
|
||||
while (iterator.isNotEmpty() && iterator.peek()?.toInt()?.toChar() != END_INDICATOR) {
|
||||
decode()?.let { nextElement ->
|
||||
listElements += nextElement
|
||||
}
|
||||
}
|
||||
iterator.pop() // drop `e`
|
||||
return BencodeList(listElements)
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a dict element from iterator assumed to have structure `d{data}e`
|
||||
*/
|
||||
private fun decodeDict(): BencodeElement? {
|
||||
iterator.pop() // drop `d`
|
||||
val dictElements = mutableMapOf<String,BencodeElement>()
|
||||
while (iterator.isNotEmpty() && iterator.peek()?.toInt()?.toChar() != END_INDICATOR) {
|
||||
val key = decodeString() ?: return null
|
||||
val value = decode() ?: return null
|
||||
dictElements += key.value.decodeToString() to value
|
||||
}
|
||||
iterator.pop() // drop `e`
|
||||
return BencodeDict(dictElements)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val NUMBERS = arrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9')
|
||||
private const val INT_INDICATOR = 'i'
|
||||
private const val LIST_INDICATOR = 'l'
|
||||
private const val DICT_INDICATOR = 'd'
|
||||
private const val END_INDICATOR = 'e'
|
||||
private const val SEPARATOR = ':'
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
sealed class BencodeElement {
|
||||
abstract fun encode(): ByteArray
|
||||
}
|
||||
|
||||
fun String.bencode() = BencodeString(this.encodeToByteArray())
|
||||
fun Int.bencode() = BencodeInteger(this)
|
||||
|
||||
data class BencodeString(val value: ByteArray): BencodeElement() {
|
||||
override fun encode(): ByteArray = buildString {
|
||||
append(value.size.toString())
|
||||
append(':')
|
||||
}.toByteArray() + value
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as BencodeString
|
||||
|
||||
if (!value.contentEquals(other.value)) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return value.contentHashCode()
|
||||
}
|
||||
}
|
||||
data class BencodeInteger(val value: Int): BencodeElement() {
|
||||
override fun encode(): ByteArray = buildString {
|
||||
append('i')
|
||||
append(value.toString())
|
||||
append('e')
|
||||
}.toByteArray()
|
||||
}
|
||||
data class BencodeList(val values: List<BencodeElement>): BencodeElement() {
|
||||
|
||||
constructor(vararg values: BencodeElement) : this(values.toList())
|
||||
|
||||
override fun encode(): ByteArray = "l".toByteArray() +
|
||||
values.fold(byteArrayOf()) { array, element -> array + element.encode() } +
|
||||
"e".toByteArray()
|
||||
}
|
||||
data class BencodeDict(val values: Map<String, BencodeElement>): BencodeElement() {
|
||||
|
||||
constructor(vararg values: Pair<String, BencodeElement>) : this(values.toMap())
|
||||
|
||||
override fun encode(): ByteArray = "d".toByteArray() +
|
||||
values.entries.fold(byteArrayOf()) { array, (key, value) ->
|
||||
array + key.bencode().encode() + value.encode()
|
||||
} + "e".toByteArray()
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as BencodeDict
|
||||
|
||||
if (values != other.values) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return values.hashCode()
|
||||
}
|
||||
|
||||
|
||||
}
|
@@ -70,7 +70,7 @@ public class Recipient implements RecipientModifiedListener {
|
||||
private final @NonNull Address address;
|
||||
private final @NonNull List<Recipient> participants = new LinkedList<>();
|
||||
|
||||
private Context context;
|
||||
private final Context context;
|
||||
private @Nullable String name;
|
||||
private @Nullable String customLabel;
|
||||
private boolean resolving;
|
||||
@@ -86,6 +86,7 @@ public class Recipient implements RecipientModifiedListener {
|
||||
private boolean blocked = false;
|
||||
private boolean approved = false;
|
||||
private boolean approvedMe = false;
|
||||
private DisappearingState disappearingState = null;
|
||||
private VibrateState messageVibrate = VibrateState.DEFAULT;
|
||||
private VibrateState callVibrate = VibrateState.DEFAULT;
|
||||
private int expireMessages = 0;
|
||||
@@ -99,6 +100,8 @@ public class Recipient implements RecipientModifiedListener {
|
||||
private boolean profileSharing;
|
||||
private String notificationChannel;
|
||||
private boolean forceSmsSelection;
|
||||
private String wrapperHash;
|
||||
private boolean blocksCommunityMessageRequests;
|
||||
|
||||
private @NonNull UnidentifiedAccessMode unidentifiedAccessMode = UnidentifiedAccessMode.ENABLED;
|
||||
|
||||
@@ -129,7 +132,7 @@ public class Recipient implements RecipientModifiedListener {
|
||||
@NonNull Optional<RecipientDetails> details,
|
||||
@NonNull ListenableFutureTask<RecipientDetails> future)
|
||||
{
|
||||
this.context = context;
|
||||
this.context = context.getApplicationContext();
|
||||
this.address = address;
|
||||
this.color = null;
|
||||
this.resolving = true;
|
||||
@@ -161,6 +164,7 @@ public class Recipient implements RecipientModifiedListener {
|
||||
this.unidentifiedAccessMode = stale.unidentifiedAccessMode;
|
||||
this.forceSmsSelection = stale.forceSmsSelection;
|
||||
this.notifyType = stale.notifyType;
|
||||
this.disappearingState = stale.disappearingState;
|
||||
|
||||
this.participants.clear();
|
||||
this.participants.addAll(stale.participants);
|
||||
@@ -191,6 +195,8 @@ public class Recipient implements RecipientModifiedListener {
|
||||
this.unidentifiedAccessMode = details.get().unidentifiedAccessMode;
|
||||
this.forceSmsSelection = details.get().forceSmsSelection;
|
||||
this.notifyType = details.get().notifyType;
|
||||
this.blocksCommunityMessageRequests = details.get().blocksCommunityMessageRequests;
|
||||
this.disappearingState = details.get().disappearingState;
|
||||
|
||||
this.participants.clear();
|
||||
this.participants.addAll(details.get().participants);
|
||||
@@ -227,6 +233,8 @@ public class Recipient implements RecipientModifiedListener {
|
||||
Recipient.this.unidentifiedAccessMode = result.unidentifiedAccessMode;
|
||||
Recipient.this.forceSmsSelection = result.forceSmsSelection;
|
||||
Recipient.this.notifyType = result.notifyType;
|
||||
Recipient.this.disappearingState = result.disappearingState;
|
||||
Recipient.this.blocksCommunityMessageRequests = result.blocksCommunityMessageRequests;
|
||||
|
||||
Recipient.this.participants.clear();
|
||||
Recipient.this.participants.addAll(result.participants);
|
||||
@@ -251,7 +259,7 @@ public class Recipient implements RecipientModifiedListener {
|
||||
}
|
||||
|
||||
Recipient(@NonNull Context context, @NonNull Address address, @NonNull RecipientDetails details) {
|
||||
this.context = context;
|
||||
this.context = context.getApplicationContext();
|
||||
this.address = address;
|
||||
this.contactUri = details.contactUri;
|
||||
this.name = details.name;
|
||||
@@ -279,6 +287,8 @@ public class Recipient implements RecipientModifiedListener {
|
||||
this.profileSharing = details.profileSharing;
|
||||
this.unidentifiedAccessMode = details.unidentifiedAccessMode;
|
||||
this.forceSmsSelection = details.forceSmsSelection;
|
||||
this.wrapperHash = details.wrapperHash;
|
||||
this.blocksCommunityMessageRequests = details.blocksCommunityMessageRequests;
|
||||
|
||||
this.participants.addAll(details.participants);
|
||||
this.resolving = false;
|
||||
@@ -319,13 +329,13 @@ public class Recipient implements RecipientModifiedListener {
|
||||
return this.name;
|
||||
}
|
||||
} else if (isOpenGroupInboxRecipient()){
|
||||
String inboxID = GroupUtil.getDecodedOpenGroupInbox(sessionID);
|
||||
String inboxID = GroupUtil.getDecodedOpenGroupInboxSessionId(sessionID);
|
||||
Contact contact = storage.getContactWithSessionID(inboxID);
|
||||
if (contact == null) { return sessionID; }
|
||||
return contact.displayName(Contact.ContactContext.REGULAR);
|
||||
} else {
|
||||
Contact contact = storage.getContactWithSessionID(sessionID);
|
||||
if (contact == null) { return sessionID; }
|
||||
if (contact == null) { return null; }
|
||||
return contact.displayName(Contact.ContactContext.REGULAR);
|
||||
}
|
||||
}
|
||||
@@ -343,6 +353,18 @@ public class Recipient implements RecipientModifiedListener {
|
||||
if (notify) notifyListeners();
|
||||
}
|
||||
|
||||
public boolean getBlocksCommunityMessageRequests() {
|
||||
return blocksCommunityMessageRequests;
|
||||
}
|
||||
|
||||
public void setBlocksCommunityMessageRequests(boolean blocksCommunityMessageRequests) {
|
||||
synchronized (this) {
|
||||
this.blocksCommunityMessageRequests = blocksCommunityMessageRequests;
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
public synchronized @NonNull MaterialColor getColor() {
|
||||
if (isGroupRecipient()) return MaterialColor.GROUP;
|
||||
else if (color != null) return color;
|
||||
@@ -435,13 +457,18 @@ public class Recipient implements RecipientModifiedListener {
|
||||
public boolean isContactRecipient() {
|
||||
return address.isContact();
|
||||
}
|
||||
public boolean is1on1() { return address.isContact() && !isLocalNumber; }
|
||||
|
||||
public boolean isOpenGroupRecipient() {
|
||||
return address.isOpenGroup();
|
||||
public boolean isCommunityRecipient() {
|
||||
return address.isCommunity();
|
||||
}
|
||||
|
||||
public boolean isOpenGroupOutboxRecipient() {
|
||||
return address.isCommunityOutbox();
|
||||
}
|
||||
|
||||
public boolean isOpenGroupInboxRecipient() {
|
||||
return address.isOpenGroupInbox();
|
||||
return address.isCommunityInbox();
|
||||
}
|
||||
|
||||
public boolean isClosedGroupRecipient() {
|
||||
@@ -483,7 +510,13 @@ public class Recipient implements RecipientModifiedListener {
|
||||
|
||||
public synchronized String toShortString() {
|
||||
String name = getName();
|
||||
return (name != null ? name : address.serialize());
|
||||
if (name != null) return name;
|
||||
String sessionId = address.serialize();
|
||||
if (sessionId.length() < 4) return sessionId; // so substrings don't throw out of bounds exceptions
|
||||
int takeAmount = 4;
|
||||
String start = sessionId.substring(0, takeAmount);
|
||||
String end = sessionId.substring(sessionId.length()-takeAmount);
|
||||
return start+"..."+end;
|
||||
}
|
||||
|
||||
public synchronized @NonNull Drawable getFallbackContactPhotoDrawable(Context context, boolean inverted) {
|
||||
@@ -653,6 +686,18 @@ public class Recipient implements RecipientModifiedListener {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
public synchronized DisappearingState getDisappearingState() {
|
||||
return disappearingState;
|
||||
}
|
||||
|
||||
public void setDisappearingState(DisappearingState disappearingState) {
|
||||
synchronized (this) {
|
||||
this.disappearingState = disappearingState;
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
public synchronized RegisteredState getRegistered() {
|
||||
if (isPushGroupRecipient()) return RegisteredState.REGISTERED;
|
||||
|
||||
@@ -717,6 +762,14 @@ public class Recipient implements RecipientModifiedListener {
|
||||
return unidentifiedAccessMode;
|
||||
}
|
||||
|
||||
public String getWrapperHash() {
|
||||
return wrapperHash;
|
||||
}
|
||||
|
||||
public void setWrapperHash(String wrapperHash) {
|
||||
this.wrapperHash = wrapperHash;
|
||||
}
|
||||
|
||||
public void setUnidentifiedAccessMode(@NonNull UnidentifiedAccessMode unidentifiedAccessMode) {
|
||||
synchronized (this) {
|
||||
this.unidentifiedAccessMode = unidentifiedAccessMode;
|
||||
@@ -734,17 +787,52 @@ public class Recipient implements RecipientModifiedListener {
|
||||
return this;
|
||||
}
|
||||
|
||||
public synchronized boolean showCallMenu() {
|
||||
return !isGroupRecipient() && hasApprovedMe();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
Recipient recipient = (Recipient) o;
|
||||
return resolving == recipient.resolving && mutedUntil == recipient.mutedUntil && notifyType == recipient.notifyType && blocked == recipient.blocked && approved == recipient.approved && approvedMe == recipient.approvedMe && expireMessages == recipient.expireMessages && address.equals(recipient.address) && Objects.equals(name, recipient.name) && Objects.equals(customLabel, recipient.customLabel) && Objects.equals(groupAvatarId, recipient.groupAvatarId) && Arrays.equals(profileKey, recipient.profileKey) && Objects.equals(profileName, recipient.profileName) && Objects.equals(profileAvatar, recipient.profileAvatar);
|
||||
return resolving == recipient.resolving
|
||||
&& mutedUntil == recipient.mutedUntil
|
||||
&& notifyType == recipient.notifyType
|
||||
&& blocked == recipient.blocked
|
||||
&& approved == recipient.approved
|
||||
&& approvedMe == recipient.approvedMe
|
||||
&& expireMessages == recipient.expireMessages
|
||||
&& address.equals(recipient.address)
|
||||
&& Objects.equals(name, recipient.name)
|
||||
&& Objects.equals(customLabel, recipient.customLabel)
|
||||
&& Objects.equals(groupAvatarId, recipient.groupAvatarId)
|
||||
&& Arrays.equals(profileKey, recipient.profileKey)
|
||||
&& Objects.equals(profileName, recipient.profileName)
|
||||
&& Objects.equals(profileAvatar, recipient.profileAvatar)
|
||||
&& Objects.equals(wrapperHash, recipient.wrapperHash)
|
||||
&& blocksCommunityMessageRequests == recipient.blocksCommunityMessageRequests;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = Objects.hash(address, name, customLabel, resolving, groupAvatarId, mutedUntil, notifyType, blocked, approved, approvedMe, expireMessages, profileName, profileAvatar);
|
||||
int result = Objects.hash(
|
||||
address,
|
||||
name,
|
||||
customLabel,
|
||||
resolving,
|
||||
groupAvatarId,
|
||||
mutedUntil,
|
||||
notifyType,
|
||||
blocked,
|
||||
approved,
|
||||
approvedMe,
|
||||
expireMessages,
|
||||
profileName,
|
||||
profileAvatar,
|
||||
wrapperHash,
|
||||
blocksCommunityMessageRequests
|
||||
);
|
||||
result = 31 * result + Arrays.hashCode(profileKey);
|
||||
return result;
|
||||
}
|
||||
@@ -787,6 +875,24 @@ public class Recipient implements RecipientModifiedListener {
|
||||
}
|
||||
}
|
||||
|
||||
public enum DisappearingState {
|
||||
LEGACY(0), UPDATED(1);
|
||||
|
||||
private final int id;
|
||||
|
||||
DisappearingState(int id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public static DisappearingState fromId(int id) {
|
||||
return values()[id];
|
||||
}
|
||||
}
|
||||
|
||||
public enum RegisteredState {
|
||||
UNKNOWN(0), REGISTERED(1), NOT_REGISTERED(2);
|
||||
|
||||
@@ -829,6 +935,7 @@ public class Recipient implements RecipientModifiedListener {
|
||||
private final boolean approvedMe;
|
||||
private final long muteUntil;
|
||||
private final int notifyType;
|
||||
private final DisappearingState disappearingState;
|
||||
private final VibrateState messageVibrateState;
|
||||
private final VibrateState callVibrateState;
|
||||
private final Uri messageRingtone;
|
||||
@@ -848,53 +955,62 @@ public class Recipient implements RecipientModifiedListener {
|
||||
private final String notificationChannel;
|
||||
private final UnidentifiedAccessMode unidentifiedAccessMode;
|
||||
private final boolean forceSmsSelection;
|
||||
private final String wrapperHash;
|
||||
private final boolean blocksCommunityMessageRequests;
|
||||
|
||||
public RecipientSettings(boolean blocked, boolean approved, boolean approvedMe, long muteUntil,
|
||||
int notifyType,
|
||||
int notifyType,
|
||||
@NonNull DisappearingState disappearingState,
|
||||
@NonNull VibrateState messageVibrateState,
|
||||
@NonNull VibrateState callVibrateState,
|
||||
@Nullable Uri messageRingtone,
|
||||
@Nullable Uri callRingtone,
|
||||
@Nullable MaterialColor color,
|
||||
int defaultSubscriptionId,
|
||||
int expireMessages,
|
||||
@NonNull RegisteredState registered,
|
||||
@Nullable byte[] profileKey,
|
||||
@Nullable String systemDisplayName,
|
||||
@Nullable String systemContactPhoto,
|
||||
@Nullable String systemPhoneLabel,
|
||||
@Nullable String systemContactUri,
|
||||
@Nullable String signalProfileName,
|
||||
@Nullable String signalProfileAvatar,
|
||||
boolean profileSharing,
|
||||
@Nullable String notificationChannel,
|
||||
@NonNull UnidentifiedAccessMode unidentifiedAccessMode,
|
||||
boolean forceSmsSelection)
|
||||
@NonNull VibrateState callVibrateState,
|
||||
@Nullable Uri messageRingtone,
|
||||
@Nullable Uri callRingtone,
|
||||
@Nullable MaterialColor color,
|
||||
int defaultSubscriptionId,
|
||||
int expireMessages,
|
||||
@NonNull RegisteredState registered,
|
||||
@Nullable byte[] profileKey,
|
||||
@Nullable String systemDisplayName,
|
||||
@Nullable String systemContactPhoto,
|
||||
@Nullable String systemPhoneLabel,
|
||||
@Nullable String systemContactUri,
|
||||
@Nullable String signalProfileName,
|
||||
@Nullable String signalProfileAvatar,
|
||||
boolean profileSharing,
|
||||
@Nullable String notificationChannel,
|
||||
@NonNull UnidentifiedAccessMode unidentifiedAccessMode,
|
||||
boolean forceSmsSelection,
|
||||
String wrapperHash,
|
||||
boolean blocksCommunityMessageRequests
|
||||
)
|
||||
{
|
||||
this.blocked = blocked;
|
||||
this.approved = approved;
|
||||
this.approvedMe = approvedMe;
|
||||
this.muteUntil = muteUntil;
|
||||
this.notifyType = notifyType;
|
||||
this.messageVibrateState = messageVibrateState;
|
||||
this.callVibrateState = callVibrateState;
|
||||
this.messageRingtone = messageRingtone;
|
||||
this.callRingtone = callRingtone;
|
||||
this.color = color;
|
||||
this.defaultSubscriptionId = defaultSubscriptionId;
|
||||
this.expireMessages = expireMessages;
|
||||
this.registered = registered;
|
||||
this.profileKey = profileKey;
|
||||
this.systemDisplayName = systemDisplayName;
|
||||
this.systemContactPhoto = systemContactPhoto;
|
||||
this.systemPhoneLabel = systemPhoneLabel;
|
||||
this.systemContactUri = systemContactUri;
|
||||
this.signalProfileName = signalProfileName;
|
||||
this.signalProfileAvatar = signalProfileAvatar;
|
||||
this.profileSharing = profileSharing;
|
||||
this.notificationChannel = notificationChannel;
|
||||
this.unidentifiedAccessMode = unidentifiedAccessMode;
|
||||
this.forceSmsSelection = forceSmsSelection;
|
||||
this.blocked = blocked;
|
||||
this.approved = approved;
|
||||
this.approvedMe = approvedMe;
|
||||
this.muteUntil = muteUntil;
|
||||
this.notifyType = notifyType;
|
||||
this.disappearingState = disappearingState;
|
||||
this.messageVibrateState = messageVibrateState;
|
||||
this.callVibrateState = callVibrateState;
|
||||
this.messageRingtone = messageRingtone;
|
||||
this.callRingtone = callRingtone;
|
||||
this.color = color;
|
||||
this.defaultSubscriptionId = defaultSubscriptionId;
|
||||
this.expireMessages = expireMessages;
|
||||
this.registered = registered;
|
||||
this.profileKey = profileKey;
|
||||
this.systemDisplayName = systemDisplayName;
|
||||
this.systemContactPhoto = systemContactPhoto;
|
||||
this.systemPhoneLabel = systemPhoneLabel;
|
||||
this.systemContactUri = systemContactUri;
|
||||
this.signalProfileName = signalProfileName;
|
||||
this.signalProfileAvatar = signalProfileAvatar;
|
||||
this.profileSharing = profileSharing;
|
||||
this.notificationChannel = notificationChannel;
|
||||
this.unidentifiedAccessMode = unidentifiedAccessMode;
|
||||
this.forceSmsSelection = forceSmsSelection;
|
||||
this.wrapperHash = wrapperHash;
|
||||
this.blocksCommunityMessageRequests = blocksCommunityMessageRequests;
|
||||
}
|
||||
|
||||
public @Nullable MaterialColor getColor() {
|
||||
@@ -921,6 +1037,10 @@ public class Recipient implements RecipientModifiedListener {
|
||||
return notifyType;
|
||||
}
|
||||
|
||||
public @NonNull DisappearingState getDisappearingState() {
|
||||
return disappearingState;
|
||||
}
|
||||
|
||||
public @NonNull VibrateState getMessageVibrateState() {
|
||||
return messageVibrateState;
|
||||
}
|
||||
@@ -992,6 +1112,15 @@ public class Recipient implements RecipientModifiedListener {
|
||||
public boolean isForceSmsSelection() {
|
||||
return forceSmsSelection;
|
||||
}
|
||||
|
||||
public String getWrapperHash() {
|
||||
return wrapperHash;
|
||||
}
|
||||
|
||||
public boolean getBlocksCommunityMessageRequests() {
|
||||
return blocksCommunityMessageRequests;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
@@ -31,6 +31,7 @@ import org.session.libsession.utilities.ListenableFutureTask;
|
||||
import org.session.libsession.utilities.MaterialColor;
|
||||
import org.session.libsession.utilities.TextSecurePreferences;
|
||||
import org.session.libsession.utilities.Util;
|
||||
import org.session.libsession.utilities.recipients.Recipient.DisappearingState;
|
||||
import org.session.libsession.utilities.recipients.Recipient.RecipientSettings;
|
||||
import org.session.libsession.utilities.recipients.Recipient.RegisteredState;
|
||||
import org.session.libsession.utilities.recipients.Recipient.UnidentifiedAccessMode;
|
||||
@@ -159,6 +160,7 @@ class RecipientProvider {
|
||||
@Nullable final Uri callRingtone;
|
||||
final long mutedUntil;
|
||||
final int notifyType;
|
||||
@Nullable final DisappearingState disappearingState;
|
||||
@Nullable final VibrateState messageVibrateState;
|
||||
@Nullable final VibrateState callVibrateState;
|
||||
final boolean blocked;
|
||||
@@ -177,6 +179,8 @@ class RecipientProvider {
|
||||
@Nullable final String notificationChannel;
|
||||
@NonNull final UnidentifiedAccessMode unidentifiedAccessMode;
|
||||
final boolean forceSmsSelection;
|
||||
final String wrapperHash;
|
||||
final boolean blocksCommunityMessageRequests;
|
||||
|
||||
RecipientDetails(@Nullable String name, @Nullable Long groupAvatarId,
|
||||
boolean systemContact, boolean isLocalNumber, @Nullable RecipientSettings settings,
|
||||
@@ -191,6 +195,7 @@ class RecipientProvider {
|
||||
this.callRingtone = settings != null ? settings.getCallRingtone() : null;
|
||||
this.mutedUntil = settings != null ? settings.getMuteUntil() : 0;
|
||||
this.notifyType = settings != null ? settings.getNotifyType() : 0;
|
||||
this.disappearingState = settings != null ? settings.getDisappearingState() : null;
|
||||
this.messageVibrateState = settings != null ? settings.getMessageVibrateState() : null;
|
||||
this.callVibrateState = settings != null ? settings.getCallVibrateState() : null;
|
||||
this.blocked = settings != null && settings.isBlocked();
|
||||
@@ -209,6 +214,8 @@ class RecipientProvider {
|
||||
this.notificationChannel = settings != null ? settings.getNotificationChannel() : null;
|
||||
this.unidentifiedAccessMode = settings != null ? settings.getUnidentifiedAccessMode() : UnidentifiedAccessMode.DISABLED;
|
||||
this.forceSmsSelection = settings != null && settings.isForceSmsSelection();
|
||||
this.wrapperHash = settings != null ? settings.getWrapperHash() : null;
|
||||
this.blocksCommunityMessageRequests = settings != null && settings.getBlocksCommunityMessageRequests();
|
||||
|
||||
if (name == null && settings != null) this.name = settings.getSystemDisplayName();
|
||||
else this.name = name;
|
||||
|
50
libsession/src/main/res/values-fr-rFR/strings.xml
Normal file
50
libsession/src/main/res/values-fr-rFR/strings.xml
Normal file
@@ -0,0 +1,50 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- MessageRecord -->
|
||||
<string name="MessageRecord_left_group">Vous avez quitté le groupe.</string>
|
||||
<string name="MessageRecord_you_created_a_new_group">Vous avez créé un nouveau groupe.</string>
|
||||
<string name="MessageRecord_s_added_you_to_the_group">%1$s vous a ajouté·e dans le groupe.</string>
|
||||
<string name="MessageRecord_you_renamed_the_group_to_s">Vous avez renommé le groupe en %1$s</string>
|
||||
<string name="MessageRecord_s_renamed_the_group_to_s">%1$s a renommé le groupe en : %2$s</string>
|
||||
<string name="MessageRecord_you_added_s_to_the_group">Vous avez ajouté %1$s au groupe.</string>
|
||||
<string name="MessageRecord_s_added_s_to_the_group">%1$s a ajouté %2$s au groupe.</string>
|
||||
<string name="MessageRecord_you_removed_s_from_the_group">Vous avez retiré %1$s du groupe.</string>
|
||||
<string name="MessageRecord_s_removed_s_from_the_group">%1$s a supprimé %2$s du groupe.</string>
|
||||
<string name="MessageRecord_you_were_removed_from_the_group">Vous avez été retiré·e du groupe.</string>
|
||||
<string name="MessageRecord_you">Vous</string>
|
||||
<string name="MessageRecord_s_called_you">%s vous a appelé·e</string>
|
||||
<string name="MessageRecord_called_s">Vous avez appelé %s</string>
|
||||
<string name="MessageRecord_missed_call_from">Appel manqué de %s</string>
|
||||
<string name="MessageRecord_s_took_a_screenshot">%1$s a pris une capture d\'écran.</string>
|
||||
<string name="MessageRecord_media_saved_by_s">%1$s a enregistré le média.</string>
|
||||
<!-- expiration -->
|
||||
<string name="expiration_off">Désactivé</string>
|
||||
<plurals name="expiration_seconds">
|
||||
<item quantity="one">%d seconde</item>
|
||||
<item quantity="other">%d secondes</item>
|
||||
</plurals>
|
||||
<string name="expiration_seconds_abbreviated">%d s</string>
|
||||
<plurals name="expiration_minutes">
|
||||
<item quantity="one">%d minute</item>
|
||||
<item quantity="other">%d minutes</item>
|
||||
</plurals>
|
||||
<string name="expiration_minutes_abbreviated">%d min</string>
|
||||
<plurals name="expiration_hours">
|
||||
<item quantity="one">%d heure</item>
|
||||
<item quantity="other">%d heures</item>
|
||||
</plurals>
|
||||
<string name="expiration_hours_abbreviated">%d h</string>
|
||||
<plurals name="expiration_days">
|
||||
<item quantity="one">%d jour</item>
|
||||
<item quantity="other">%d jours</item>
|
||||
</plurals>
|
||||
<string name="expiration_days_abbreviated">%d j</string>
|
||||
<plurals name="expiration_weeks">
|
||||
<item quantity="one">%d semaine</item>
|
||||
<item quantity="other">%d semaines</item>
|
||||
</plurals>
|
||||
<string name="expiration_weeks_abbreviated">%d sem</string>
|
||||
<string name="ConversationItem_group_action_left">%1$s a quitté le groupe.</string>
|
||||
<!-- RecipientProvider -->
|
||||
<string name="RecipientProvider_unnamed_group">Groupe sans nom</string>
|
||||
</resources>
|
50
libsession/src/main/res/values-fr/strings.xml
Normal file
50
libsession/src/main/res/values-fr/strings.xml
Normal file
@@ -0,0 +1,50 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- MessageRecord -->
|
||||
<string name="MessageRecord_left_group">Vous avez quitté le groupe.</string>
|
||||
<string name="MessageRecord_you_created_a_new_group">Vous avez créé un nouveau groupe.</string>
|
||||
<string name="MessageRecord_s_added_you_to_the_group">%1$s vous a ajouté·e dans le groupe.</string>
|
||||
<string name="MessageRecord_you_renamed_the_group_to_s">Vous avez renommé le groupe en %1$s</string>
|
||||
<string name="MessageRecord_s_renamed_the_group_to_s">%1$s a renommé le groupe en : %2$s</string>
|
||||
<string name="MessageRecord_you_added_s_to_the_group">Vous avez ajouté %1$s au groupe.</string>
|
||||
<string name="MessageRecord_s_added_s_to_the_group">%1$s a ajouté %2$s au groupe.</string>
|
||||
<string name="MessageRecord_you_removed_s_from_the_group">Vous avez retiré %1$s du groupe.</string>
|
||||
<string name="MessageRecord_s_removed_s_from_the_group">%1$s a supprimé %2$s du groupe.</string>
|
||||
<string name="MessageRecord_you_were_removed_from_the_group">Vous avez été retiré·e du groupe.</string>
|
||||
<string name="MessageRecord_you">Vous</string>
|
||||
<string name="MessageRecord_s_called_you">%s vous a appelé·e</string>
|
||||
<string name="MessageRecord_called_s">Vous avez appelé %s</string>
|
||||
<string name="MessageRecord_missed_call_from">Appel manqué de %s</string>
|
||||
<string name="MessageRecord_s_took_a_screenshot">%1$s a pris une capture d\'écran.</string>
|
||||
<string name="MessageRecord_media_saved_by_s">%1$s a enregistré le média.</string>
|
||||
<!-- expiration -->
|
||||
<string name="expiration_off">Désactivé</string>
|
||||
<plurals name="expiration_seconds">
|
||||
<item quantity="one">%d seconde</item>
|
||||
<item quantity="other">%d secondes</item>
|
||||
</plurals>
|
||||
<string name="expiration_seconds_abbreviated">%d s</string>
|
||||
<plurals name="expiration_minutes">
|
||||
<item quantity="one">%d minute</item>
|
||||
<item quantity="other">%d minutes</item>
|
||||
</plurals>
|
||||
<string name="expiration_minutes_abbreviated">%d min</string>
|
||||
<plurals name="expiration_hours">
|
||||
<item quantity="one">%d heure</item>
|
||||
<item quantity="other">%d heures</item>
|
||||
</plurals>
|
||||
<string name="expiration_hours_abbreviated">%d h</string>
|
||||
<plurals name="expiration_days">
|
||||
<item quantity="one">%d jour</item>
|
||||
<item quantity="other">%d jours</item>
|
||||
</plurals>
|
||||
<string name="expiration_days_abbreviated">%d j</string>
|
||||
<plurals name="expiration_weeks">
|
||||
<item quantity="one">%d semaine</item>
|
||||
<item quantity="other">%d semaines</item>
|
||||
</plurals>
|
||||
<string name="expiration_weeks_abbreviated">%d sem</string>
|
||||
<string name="ConversationItem_group_action_left">%1$s a quitté le groupe.</string>
|
||||
<!-- RecipientProvider -->
|
||||
<string name="RecipientProvider_unnamed_group">Groupe sans nom</string>
|
||||
</resources>
|
@@ -220,18 +220,6 @@
|
||||
<attr name="emoji_maxLength" format="integer" />
|
||||
</declare-styleable>
|
||||
|
||||
<declare-styleable name="ColorPickerPreference">
|
||||
<attr name="currentColor" format="reference" />
|
||||
<attr name="colors" format="reference" />
|
||||
<attr name="sortColors" format="boolean|reference" />
|
||||
<attr name="colorDescriptions" format="reference" />
|
||||
<attr name="columns" format="integer|reference" />
|
||||
<attr name="colorSize" format="enum|reference">
|
||||
<enum name="large" value="1" />
|
||||
<enum name="small" value="2" />
|
||||
</attr>
|
||||
</declare-styleable>
|
||||
|
||||
<declare-styleable name="VerificationCodeView">
|
||||
<attr name="vcv_spacing" format="dimension"/>
|
||||
<attr name="vcv_inputWidth" format="dimension"/>
|
||||
@@ -264,11 +252,6 @@
|
||||
<attr name="doc_downloadButtonTint" format="color" />
|
||||
</declare-styleable>
|
||||
|
||||
<declare-styleable name="ConversationItemFooter">
|
||||
<attr name="footer_text_color" format="color" />
|
||||
<attr name="footer_icon_color" format="color" />
|
||||
</declare-styleable>
|
||||
|
||||
<declare-styleable name="ConversationItemThumbnail">
|
||||
<attr name="conversationThumbnail_minWidth" format="dimension" />
|
||||
<attr name="conversationThumbnail_maxWidth" format="dimension" />
|
||||
|
@@ -15,12 +15,22 @@
|
||||
<string name="MessageRecord_s_called_you">%s called you</string>
|
||||
<string name="MessageRecord_called_s">Called %s</string>
|
||||
<string name="MessageRecord_missed_call_from">Missed call from %s</string>
|
||||
<string name="MessageRecord_you_disabled_disappearing_messages">You disabled disappearing messages.</string>
|
||||
<string name="MessageRecord_s_disabled_disappearing_messages">%1$s disabled disappearing messages.</string>
|
||||
<string name="MessageRecord_you_set_disappearing_message_time_to_s">You set the disappearing message timer to %1$s</string>
|
||||
<string name="MessageRecord_s_set_disappearing_message_time_to_s">%1$s set the disappearing message timer to %2$s</string>
|
||||
<string name="MessageRecord_follow_setting">Follow Setting</string>
|
||||
<string name="AccessibilityId_follow_setting">Follow setting</string>
|
||||
<string name="MessageRecord_you_turned_off_disappearing_messages">You have turned off disappearing messages.</string>
|
||||
<string name="MessageRecord_you_turned_off_disappearing_messages_1_on_1">You turned off disappearing messages. Messages you send will no longer disappear.</string>
|
||||
<string name="MessageRecord_s_turned_off_disappearing_messages">%1$s turned off disappearing messages.</string>
|
||||
<string name="MessageRecord_s_turned_off_disappearing_messages_1_on_1">%1$s has turned off disappearing messages. Messages they send will no longer disappear.</string>
|
||||
<string name="MessageRecord_you_set_messages_to_disappear_s_after_s">You have set messages to disappear %1$s after they have been %2$s</string>
|
||||
<string name="MessageRecord_you_set_messages_to_disappear_s_after_s_1_on_1">You set your messages to disappear %1$s after they have been %2$s.</string>
|
||||
<string name="MessageRecord_you_changed_messages_to_disappear_s_after_s">You have changed messages to disappear %1$s after they have been %2$s</string>
|
||||
<string name="MessageRecord_s_set_messages_to_disappear_s_after_s">%1$s has set messages to disappear %2$s after they have been %3$s</string>
|
||||
<string name="MessageRecord_s_set_messages_to_disappear_s_after_s_1_on_1">%1$s has set their messages to disappear %2$s after they have been %3$s.</string>
|
||||
<string name="MessageRecord_s_changed_messages_to_disappear_s_after_s">%1$s has changed messages to disappear %2$s after they have been %3$s</string>
|
||||
<string name="MessageRecord_s_took_a_screenshot">%1$s took a screenshot.</string>
|
||||
<string name="MessageRecord_media_saved_by_s">Media saved by %1$s.</string>
|
||||
<string name="MessageRecord_state_read">read</string>
|
||||
<string name="MessageRecord_state_sent">sent</string>
|
||||
|
||||
<!-- expiration -->
|
||||
<string name="expiration_off">Off</string>
|
||||
|
@@ -2,4 +2,5 @@
|
||||
<resources>
|
||||
<bool name="enable_alarm_manager">true</bool>
|
||||
<bool name="enable_job_service">false</bool>
|
||||
<bool name="screen_security_default">true</bool>
|
||||
</resources>
|
||||
|
@@ -0,0 +1,107 @@
|
||||
package org.session.libsession.utilities
|
||||
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.session.libsession.utilities.bencode.Bencode
|
||||
import org.session.libsession.utilities.bencode.BencodeDict
|
||||
import org.session.libsession.utilities.bencode.BencodeInteger
|
||||
import org.session.libsession.utilities.bencode.BencodeList
|
||||
import org.session.libsession.utilities.bencode.bencode
|
||||
|
||||
class BencoderTest {
|
||||
|
||||
@Test
|
||||
fun `it should decode a basic string`() {
|
||||
val basicString = "5:howdy".toByteArray()
|
||||
val bencoder = Bencode.Decoder(basicString)
|
||||
val result = bencoder.decode()
|
||||
assertEquals("howdy".bencode(), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `it should decode a basic integer`() {
|
||||
val basicInteger = "i3e".toByteArray()
|
||||
val bencoder = Bencode.Decoder(basicInteger)
|
||||
val result = bencoder.decode()
|
||||
assertEquals(BencodeInteger(3), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `it should decode a list of integers`() {
|
||||
val basicIntList = "li1ei2ee".toByteArray()
|
||||
val bencoder = Bencode.Decoder(basicIntList)
|
||||
val result = bencoder.decode()
|
||||
assertEquals(
|
||||
BencodeList(
|
||||
1.bencode(),
|
||||
2.bencode()
|
||||
),
|
||||
result
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `it should decode a basic dict`() {
|
||||
val basicDict = "d4:spaml1:a1:bee".toByteArray()
|
||||
val bencoder = Bencode.Decoder(basicDict)
|
||||
val result = bencoder.decode()
|
||||
assertEquals(
|
||||
BencodeDict(
|
||||
"spam" to BencodeList(
|
||||
"a".bencode(),
|
||||
"b".bencode()
|
||||
)
|
||||
),
|
||||
result
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `it should encode a basic string`() {
|
||||
val basicString = "5:howdy".toByteArray()
|
||||
val element = "howdy".bencode()
|
||||
assertArrayEquals(basicString, element.encode())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `it should encode a basic int`() {
|
||||
val basicInt = "i3e".toByteArray()
|
||||
val element = 3.bencode()
|
||||
assertArrayEquals(basicInt, element.encode())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `it should encode a basic list`() {
|
||||
val basicList = "li1ei2ee".toByteArray()
|
||||
val element = BencodeList(1.bencode(),2.bencode())
|
||||
assertArrayEquals(basicList, element.encode())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `it should encode a basic dict`() {
|
||||
val basicDict = "d4:spaml1:a1:bee".toByteArray()
|
||||
val element = BencodeDict(
|
||||
"spam" to BencodeList(
|
||||
"a".bencode(),
|
||||
"b".bencode()
|
||||
)
|
||||
)
|
||||
assertArrayEquals(basicDict, element.encode())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `it should encode a more complex real world case`() {
|
||||
val source = "d15:lastReadMessaged66:031122334455667788990011223344556677889900112233445566778899001122i1234568790e66:051122334455667788990011223344556677889900112233445566778899001122i1234568790ee5:seqNoi1ee".toByteArray()
|
||||
val result = Bencode.Decoder(source).decode()
|
||||
val expected = BencodeDict(
|
||||
"lastReadMessage" to BencodeDict(
|
||||
"051122334455667788990011223344556677889900112233445566778899001122" to 1234568790.bencode(),
|
||||
"031122334455667788990011223344556677889900112233445566778899001122" to 1234568790.bencode()
|
||||
),
|
||||
"seqNo" to BencodeInteger(1)
|
||||
)
|
||||
assertEquals(expected, result)
|
||||
}
|
||||
|
||||
}
|
@@ -1,9 +1,9 @@
|
||||
package org.session.libsession.utilities
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.junit.Assert.*
|
||||
|
||||
class OpenGroupUrlParserTest {
|
||||
class CommunityUrlParserTest {
|
||||
|
||||
@Test
|
||||
fun parseUrlTest() {
|
Reference in New Issue
Block a user