package org.thoughtcrime.securesms.database import android.content.Context import android.net.Uri import network.loki.messenger.libsession_util.Config import network.loki.messenger.libsession_util.ConfigBase.Companion.PRIORITY_HIDDEN import network.loki.messenger.libsession_util.ConfigBase.Companion.PRIORITY_PINNED import network.loki.messenger.libsession_util.ConfigBase.Companion.PRIORITY_VISIBLE import network.loki.messenger.libsession_util.Contacts import network.loki.messenger.libsession_util.ConversationVolatileConfig import network.loki.messenger.libsession_util.GroupInfoConfig import network.loki.messenger.libsession_util.GroupKeysConfig import network.loki.messenger.libsession_util.GroupMembersConfig import network.loki.messenger.libsession_util.UserGroupsConfig import network.loki.messenger.libsession_util.UserProfile import network.loki.messenger.libsession_util.util.BaseCommunityInfo import network.loki.messenger.libsession_util.util.Conversation import network.loki.messenger.libsession_util.util.ExpiryMode import network.loki.messenger.libsession_util.util.GroupInfo import network.loki.messenger.libsession_util.util.UserPic import org.session.libsession.avatars.AvatarHelper import org.session.libsession.database.StorageProtocol 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.BackgroundGroupAddJob import org.session.libsession.messaging.jobs.ConfigurationSyncJob import org.session.libsession.messaging.jobs.GroupAvatarDownloadJob import org.session.libsession.messaging.jobs.InviteContactJob import org.session.libsession.messaging.jobs.Job import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.jobs.MessageReceiveJob import org.session.libsession.messaging.jobs.MessageSendJob import org.session.libsession.messaging.jobs.RetrieveProfileAvatarJob import org.session.libsession.messaging.messages.Destination import org.session.libsession.messaging.messages.Message import org.session.libsession.messaging.messages.control.ConfigurationMessage import org.session.libsession.messaging.messages.control.GroupUpdated import org.session.libsession.messaging.messages.control.MessageRequestResponse import org.session.libsession.messaging.messages.signal.IncomingEncryptedMessage import org.session.libsession.messaging.messages.signal.IncomingGroupMessage import org.session.libsession.messaging.messages.signal.IncomingMediaMessage import org.session.libsession.messaging.messages.signal.IncomingTextMessage import org.session.libsession.messaging.messages.signal.OutgoingGroupMediaMessage import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage import org.session.libsession.messaging.messages.signal.OutgoingTextMessage 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.MessageSender 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 import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview import org.session.libsession.messaging.sending_receiving.notifications.PushRegistryV1 import org.session.libsession.messaging.sending_receiving.pollers.LegacyClosedGroupPollerV2 import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel import org.session.libsession.messaging.utilities.SodiumUtilities import org.session.libsession.messaging.utilities.UpdateMessageData import org.session.libsession.snode.OnionRequestAPI import org.session.libsession.snode.RawResponse import org.session.libsession.snode.SnodeAPI import org.session.libsession.snode.SnodeMessage 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.ProfileKeyUtil import org.session.libsession.utilities.SSKEnvironment import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.recipients.Recipient 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.messages.SignalServiceAttachmentPointer import org.session.libsignal.messages.SignalServiceGroup import org.session.libsignal.protos.SignalServiceProtos.DataMessage import org.session.libsignal.protos.SignalServiceProtos.DataMessage.GroupUpdateInviteResponseMessage import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.Hex import org.session.libsignal.utilities.IdPrefix import org.session.libsignal.utilities.KeyHelper import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.SessionId import org.session.libsignal.utilities.guava.Optional import org.session.libsignal.utilities.toHexString import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper import org.thoughtcrime.securesms.database.model.MessageId import org.thoughtcrime.securesms.database.model.ReactionRecord import org.thoughtcrime.securesms.dependencies.ConfigFactory import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.dependencies.PollerFactory import org.thoughtcrime.securesms.groups.ClosedGroupManager import org.thoughtcrime.securesms.groups.GroupManager import org.thoughtcrime.securesms.groups.OpenGroupManager import org.thoughtcrime.securesms.mms.PartAuthority import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities import org.thoughtcrime.securesms.util.SessionMetaProtocol import java.security.MessageDigest import network.loki.messenger.libsession_util.util.Contact as LibSessionContact import network.loki.messenger.libsession_util.util.GroupMember as LibSessionGroupMember open class Storage( context: Context, helper: SQLCipherOpenHelper, private val configFactory: ConfigFactory, private val pollerFactory: PollerFactory ) : Database(context, helper), StorageProtocol, ThreadDatabase.ConversationThreadUpdateListener { override fun threadCreated(address: Address, threadId: Long) { val localUserAddress = getUserPublicKey() ?: return if (!getRecipientApproved(address) && localUserAddress != address.serialize()) return // don't store unapproved / message requests val volatile = configFactory.convoVolatile ?: return if (address.isGroup) { val groups = configFactory.userGroups ?: return when { address.isLegacyClosedGroup -> { val sessionId = GroupUtil.doubleDecodeGroupId(address.serialize()) val closedGroup = getGroup(address.toGroupString()) if (closedGroup != null && closedGroup.isActive) { val legacyGroup = groups.getOrConstructLegacyGroupInfo(sessionId) groups.set(legacyGroup) val newVolatileParams = volatile.getOrConstructLegacyGroup(sessionId).copy( lastRead = SnodeAPI.nowWithOffset, ) volatile.set(newVolatileParams) } } address.isClosedGroup -> { val sessionId = address.serialize() groups.getClosedGroup(sessionId) ?: return Log.d("Closed group doesn't exist locally", NullPointerException()) val conversation = Conversation.ClosedGroup( sessionId, 0, false ) volatile.set(conversation) } address.isOpenGroup -> { // these should be added on the group join / group info fetch Log.w("Loki", "Thread created called for open group address, not adding any extra information") } } } else if (address.isContact) { // non-standard contact prefixes: 15, 00 etc shouldn't be stored in config if (SessionId(address.serialize()).prefix != IdPrefix.STANDARD) return // don't update our own address into the contacts DB if (getUserPublicKey() != address.serialize()) { val contacts = configFactory.contacts ?: return contacts.upsertContact(address.serialize()) { priority = PRIORITY_VISIBLE } } else { val userProfile = configFactory.user ?: return userProfile.setNtsPriority(PRIORITY_VISIBLE) DatabaseComponent.get(context).threadDatabase().setHasSent(threadId, true) } val newVolatileParams = volatile.getOrConstructOneToOne(address.serialize()) volatile.set(newVolatileParams) } } override fun threadDeleted(address: Address, threadId: Long) { val volatile = configFactory.convoVolatile ?: return if (address.isGroup) { val groups = configFactory.userGroups ?: return if (address.isLegacyClosedGroup) { val sessionId = GroupUtil.doubleDecodeGroupId(address.serialize()) volatile.eraseLegacyClosedGroup(sessionId) groups.eraseLegacyGroup(sessionId) } else if (address.isOpenGroup) { // these should be removed in the group leave / handling new configs Log.w("Loki", "Thread delete called for open group address, expecting to be handled elsewhere") } else if (address.isClosedGroup) { TODO("add the thread deleted checks for new closed groups") } } else { // non-standard contact prefixes: 15, 00 etc shouldn't be stored in config if (SessionId(address.serialize()).prefix != IdPrefix.STANDARD) return volatile.eraseOneToOne(address.serialize()) if (getUserPublicKey() != address.serialize()) { val contacts = configFactory.contacts ?: return contacts.upsertContact(address.serialize()) { priority = PRIORITY_HIDDEN } } else { val userProfile = configFactory.user ?: return userProfile.setNtsPriority(PRIORITY_HIDDEN) } } ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context) } override fun getUserPublicKey(): String? { return TextSecurePreferences.getLocalNumber(context) } override fun getUserX25519KeyPair(): ECKeyPair { return DatabaseComponent.get(context).lokiAPIDatabase().getUserX25519KeyPair() } override fun getUserProfile(): Profile { val displayName = TextSecurePreferences.getProfileName(context)!! val profileKey = ProfileKeyUtil.getProfileKey(context) val profilePictureUrl = TextSecurePreferences.getProfilePictureURL(context) return Profile(displayName, profileKey, profilePictureUrl) } override fun setProfileAvatar(recipient: Recipient, profileAvatar: String?) { val database = DatabaseComponent.get(context).recipientDatabase() database.setProfileAvatar(recipient, profileAvatar) } override fun setProfilePicture(recipient: Recipient, newProfilePicture: String?, newProfileKey: ByteArray?) { val db = DatabaseComponent.get(context).recipientDatabase() db.setProfileAvatar(recipient, newProfilePicture) db.setProfileKey(recipient, newProfileKey) } override fun setBlocksCommunityMessageRequests(recipient: Recipient, blocksMessageRequests: Boolean) { val db = DatabaseComponent.get(context).recipientDatabase() db.setBlocksCommunityMessageRequests(recipient, blocksMessageRequests) } override fun setUserProfilePicture(newProfilePicture: String?, newProfileKey: ByteArray?) { val ourRecipient = fromSerialized(getUserPublicKey()!!).let { Recipient.from(context, it, false) } ourRecipient.resolve().profileKey = newProfileKey TextSecurePreferences.setProfileKey(context, newProfileKey?.let { Base64.encodeBytes(it) }) TextSecurePreferences.setProfilePictureURL(context, newProfilePicture) if (newProfileKey != null) { JobQueue.shared.add(RetrieveProfileAvatarJob(newProfilePicture, ourRecipient.address)) } } override fun getOrGenerateRegistrationID(): Int { var registrationID = TextSecurePreferences.getLocalRegistrationId(context) if (registrationID == 0) { registrationID = KeyHelper.generateRegistrationId(false) TextSecurePreferences.setLocalRegistrationId(context, registrationID) } return registrationID } override fun persistAttachments(messageID: Long, attachments: List): List { val database = DatabaseComponent.get(context).attachmentDatabase() val databaseAttachments = attachments.mapNotNull { it.toSignalAttachment() } return database.insertAttachments(messageID, databaseAttachments) } override fun getAttachmentsForMessage(messageID: Long): List { val database = DatabaseComponent.get(context).attachmentDatabase() return database.getAttachmentsForMessage(messageID) } override fun getLastSeen(threadId: Long): Long { val threadDb = DatabaseComponent.get(context).threadDatabase() return threadDb.getLastSeenAndHasSent(threadId)?.first() ?: 0L } override fun markConversationAsRead(threadId: Long, lastSeenTime: Long, force: Boolean) { val threadDb = DatabaseComponent.get(context).threadDatabase() getRecipientForThread(threadId)?.let { recipient -> val currentLastRead = threadDb.getLastSeenAndHasSent(threadId).first() // don't set the last read in the volatile if we didn't set it in the DB if (!threadDb.markAllAsRead(threadId, recipient.isGroupRecipient, lastSeenTime, force) && !force) return // don't process configs for inbox recipients if (recipient.isOpenGroupInboxRecipient) return configFactory.convoVolatile?.let { config -> val convo = when { // recipient closed group recipient.isLegacyClosedGroupRecipient -> config.getOrConstructLegacyGroup(GroupUtil.doubleDecodeGroupId(recipient.address.serialize())) recipient.isClosedGroupRecipient -> config.getOrConstructClosedGroup(recipient.address.serialize()) // recipient is open group recipient.isOpenGroupRecipient -> { val openGroupJoinUrl = getOpenGroup(threadId)?.joinURL ?: return BaseCommunityInfo.parseFullUrl(openGroupJoinUrl)?.let { (base, room, pubKey) -> config.getOrConstructCommunity(base, room, pubKey) } ?: return } // otherwise recipient is one to one recipient.isContactRecipient -> { // don't process non-standard session IDs though val sessionId = SessionId(recipient.address.serialize()) if (sessionId.prefix != IdPrefix.STANDARD) return config.getOrConstructOneToOne(recipient.address.serialize()) } else -> throw NullPointerException("Weren't expecting to have a convo with address ${recipient.address.serialize()}") } convo.lastRead = lastSeenTime if (convo.unread) { convo.unread = lastSeenTime <= currentLastRead notifyConversationListListeners() } config.set(convo) ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context) } } } override fun updateThread(threadId: Long, unarchive: Boolean) { val threadDb = DatabaseComponent.get(context).threadDatabase() threadDb.update(threadId, unarchive, false) } override fun persist(message: VisibleMessage, quotes: QuoteModel?, linkPreview: List, groupPublicKey: String?, openGroupID: String?, attachments: List, runThreadUpdate: Boolean): Long? { var messageID: Long? = null val senderAddress = fromSerialized(message.sender!!) val isUserSender = (message.sender!! == getUserPublicKey()) val isUserBlindedSender = message.threadID?.takeIf { it >= 0 }?.let { getOpenGroup(it)?.publicKey } ?.let { SodiumUtilities.sessionId(getUserPublicKey()!!, message.sender!!, it) } ?: false val group: Optional = when { openGroupID != null -> Optional.of(SignalServiceGroup(openGroupID.toByteArray(), SignalServiceGroup.GroupType.PUBLIC_CHAT)) groupPublicKey != null && groupPublicKey.startsWith(IdPrefix.GROUP.value) -> { Optional.of(SignalServiceGroup(Hex.fromStringCondensed(groupPublicKey), SignalServiceGroup.GroupType.SIGNAL)) } groupPublicKey != null -> { val doubleEncoded = GroupUtil.doubleEncodeGroupID(groupPublicKey) Optional.of(SignalServiceGroup(GroupUtil.getDecodedGroupIDAsData(doubleEncoded), SignalServiceGroup.GroupType.SIGNAL)) } else -> Optional.absent() } val pointers = attachments.mapNotNull { it.toSignalAttachment() } val targetAddress = if ((isUserSender || isUserBlindedSender) && !message.syncTarget.isNullOrEmpty()) { fromSerialized(message.syncTarget!!) } else if (group.isPresent) { val idHex = group.get().groupId.toHexString() if (idHex.startsWith(IdPrefix.GROUP.value)) { fromSerialized(idHex) } else { fromSerialized(GroupUtil.getEncodedId(group.get())) } } else if (message.recipient?.startsWith(IdPrefix.GROUP.value) == true) { fromSerialized(message.recipient!!) } else { senderAddress } val targetRecipient = Recipient.from(context, targetAddress, false) if (!targetRecipient.isGroupRecipient) { if (isUserSender || isUserBlindedSender) { setRecipientApproved(targetRecipient, true) } else { setRecipientApprovedMe(targetRecipient, true) } } if (message.threadID == null && !targetRecipient.isOpenGroupRecipient) { // open group recipients should explicitly create threads message.threadID = getOrCreateThreadIdFor(targetAddress) } if (message.isMediaMessage() || attachments.isNotEmpty()) { val quote: Optional = if (quotes != null) Optional.of(quotes) else Optional.absent() val linkPreviews: Optional> = if (linkPreview.isEmpty()) Optional.absent() else Optional.of(linkPreview.mapNotNull { it!! }) val mmsDatabase = DatabaseComponent.get(context).mmsDatabase() val insertResult = if (isUserSender || isUserBlindedSender) { val mediaMessage = OutgoingMediaMessage.from(message, targetRecipient, pointers, quote.orNull(), linkPreviews.orNull()?.firstOrNull()) mmsDatabase.insertSecureDecryptedMessageOutbox(mediaMessage, message.threadID ?: -1, message.sentTimestamp!!, runThreadUpdate) } else { // It seems like we have replaced SignalServiceAttachment with SessionServiceAttachment val signalServiceAttachments = attachments.mapNotNull { it.toSignalPointer() } val mediaMessage = IncomingMediaMessage.from(message, senderAddress, targetRecipient.expireMessages * 1000L, group, signalServiceAttachments, quote, linkPreviews) mmsDatabase.insertSecureDecryptedMessageInbox(mediaMessage, message.threadID!!, message.receivedTimestamp ?: 0, runThreadUpdate) } if (insertResult.isPresent) { messageID = insertResult.get().messageId } } else { val smsDatabase = DatabaseComponent.get(context).smsDatabase() val isOpenGroupInvitation = (message.openGroupInvitation != null) val insertResult = if (isUserSender || isUserBlindedSender) { val textMessage = if (isOpenGroupInvitation) OutgoingTextMessage.fromOpenGroupInvitation(message.openGroupInvitation, targetRecipient, message.sentTimestamp) else OutgoingTextMessage.from(message, targetRecipient) smsDatabase.insertMessageOutbox(message.threadID ?: -1, textMessage, message.sentTimestamp!!, runThreadUpdate) } else { val textMessage = if (isOpenGroupInvitation) IncomingTextMessage.fromOpenGroupInvitation(message.openGroupInvitation, senderAddress, message.sentTimestamp) else IncomingTextMessage.from(message, senderAddress, group, targetRecipient.expireMessages * 1000L) val encrypted = IncomingEncryptedMessage(textMessage, textMessage.messageBody) smsDatabase.insertMessageInbox(encrypted, message.receivedTimestamp ?: 0, runThreadUpdate) } insertResult.orNull()?.let { result -> messageID = result.messageId } } message.serverHash?.let { serverHash -> messageID?.let { id -> DatabaseComponent.get(context).lokiMessageDatabase().setMessageServerHash(id, serverHash) } } return messageID } override fun persistJob(job: Job) { DatabaseComponent.get(context).sessionJobDatabase().persistJob(job) } override fun markJobAsSucceeded(jobId: String) { DatabaseComponent.get(context).sessionJobDatabase().markJobAsSucceeded(jobId) } override fun markJobAsFailedPermanently(jobId: String) { DatabaseComponent.get(context).sessionJobDatabase().markJobAsFailedPermanently(jobId) } override fun getAllPendingJobs(type: String): Map { return DatabaseComponent.get(context).sessionJobDatabase().getAllJobs(type) } override fun getAttachmentUploadJob(attachmentID: Long): AttachmentUploadJob? { return DatabaseComponent.get(context).sessionJobDatabase().getAttachmentUploadJob(attachmentID) } override fun getMessageSendJob(messageSendJobID: String): MessageSendJob? { return DatabaseComponent.get(context).sessionJobDatabase().getMessageSendJob(messageSendJobID) } override fun getMessageReceiveJob(messageReceiveJobID: String): MessageReceiveJob? { return DatabaseComponent.get(context).sessionJobDatabase().getMessageReceiveJob(messageReceiveJobID) } override fun getGroupAvatarDownloadJob(server: String, room: String, imageId: String?): GroupAvatarDownloadJob? { return DatabaseComponent.get(context).sessionJobDatabase().getGroupAvatarDownloadJob(server, room, imageId) } override fun getConfigSyncJob(destination: Destination): Job? { return DatabaseComponent.get(context).sessionJobDatabase().getAllJobs(ConfigurationSyncJob.KEY).values.firstOrNull { (it as? ConfigurationSyncJob)?.destination == destination } } override fun resumeMessageSendJobIfNeeded(messageSendJobID: String) { val job = DatabaseComponent.get(context).sessionJobDatabase().getMessageSendJob(messageSendJobID) ?: return JobQueue.shared.resumePendingSendMessage(job) } override fun isJobCanceled(job: Job): Boolean { return DatabaseComponent.get(context).sessionJobDatabase().isJobCanceled(job) } override fun cancelPendingMessageSendJobs(threadID: Long) { val jobDb = DatabaseComponent.get(context).sessionJobDatabase() jobDb.cancelPendingMessageSendJobs(threadID) } override fun getAuthToken(room: String, server: String): String? { val id = "$server.$room" return DatabaseComponent.get(context).lokiAPIDatabase().getAuthToken(id) } override fun notifyConfigUpdates(forConfigObject: Config) { notifyUpdates(forConfigObject) } override fun conversationInConfig(publicKey: String?, groupPublicKey: String?, openGroupId: String?, visibleOnly: Boolean): Boolean { return configFactory.conversationInConfig(publicKey, groupPublicKey, openGroupId, visibleOnly) } override fun canPerformConfigChange(variant: String, publicKey: String, changeTimestampMs: Long): Boolean { return configFactory.canPerformChange(variant, publicKey, changeTimestampMs) } override fun isCheckingCommunityRequests(): Boolean { return configFactory.user?.getCommunityMessageRequests() == true } private fun notifyUpdates(forConfigObject: Config) { when (forConfigObject) { is UserProfile -> updateUser(forConfigObject) is Contacts -> updateContacts(forConfigObject) is ConversationVolatileConfig -> updateConvoVolatile(forConfigObject) is UserGroupsConfig -> updateUserGroups(forConfigObject) is GroupInfoConfig -> updateGroupInfo(forConfigObject) is GroupKeysConfig -> updateGroupKeys(forConfigObject) is GroupMembersConfig -> updateGroupMembers(forConfigObject) } } private fun updateUser(userProfile: UserProfile) { val userPublicKey = getUserPublicKey() ?: return // would love to get rid of recipient and context from this val recipient = Recipient.from(context, fromSerialized(userPublicKey), false) // update name val name = userProfile.getName() ?: return val userPic = userProfile.getPic() val profileManager = SSKEnvironment.shared.profileManager if (name.isNotEmpty()) { TextSecurePreferences.setProfileName(context, name) profileManager.setName(context, recipient, name) } // update pfp if (userPic == UserPic.DEFAULT) { clearUserPic() } else if (userPic.key.isNotEmpty() && userPic.url.isNotEmpty() && TextSecurePreferences.getProfilePictureURL(context) != userPic.url) { setUserProfilePicture(userPic.url, userPic.key) } if (userProfile.getNtsPriority() == PRIORITY_HIDDEN) { // delete nts thread if needed val ourThread = getThreadId(recipient) ?: return deleteConversation(ourThread) } else { // create note to self thread if needed (?) val ourThread = getOrCreateThreadIdFor(recipient.address) DatabaseComponent.get(context).threadDatabase().setHasSent(ourThread, true) setPinned(ourThread, userProfile.getNtsPriority() > 0) } } private fun updateGroupInfo(groupInfoConfig: GroupInfoConfig) { val threadId = getOrCreateThreadIdFor(Address.fromSerialized(groupInfoConfig.id().hexString())) val recipient = getRecipientForThread(threadId) ?: return val db = DatabaseComponent.get(context).recipientDatabase() db.setProfileName(recipient, groupInfoConfig.getName()) // TODO: handle deleted group, handle delete attachment / message before a certain time } private fun updateGroupKeys(groupKeys: GroupKeysConfig) { // TODO: update something here? } private fun updateGroupMembers(groupMembers: GroupMembersConfig) { // TODO: maybe clear out some contacts or something? } private fun updateContacts(contacts: Contacts) { val extracted = contacts.all().toList() addLibSessionContacts(extracted) } override fun clearUserPic() { val userPublicKey = getUserPublicKey() ?: return val recipientDatabase = DatabaseComponent.get(context).recipientDatabase() // would love to get rid of recipient and context from this val recipient = Recipient.from(context, fromSerialized(userPublicKey), false) // clear picture if userPic is null TextSecurePreferences.setProfileKey(context, null) ProfileKeyUtil.setEncodedProfileKey(context, null) recipientDatabase.setProfileAvatar(recipient, null) TextSecurePreferences.setProfileAvatarId(context, 0) TextSecurePreferences.setProfilePictureURL(context, null) Recipient.removeCached(fromSerialized(userPublicKey)) configFactory.user?.setPic(UserPic.DEFAULT) ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context) } private fun updateConvoVolatile(convos: ConversationVolatileConfig) { val extracted = convos.all() for (conversation in extracted) { val threadId = when (conversation) { is Conversation.OneToOne -> getThreadIdFor(conversation.sessionId, null, null, createThread = false) is Conversation.LegacyGroup -> getThreadIdFor("", conversation.groupId,null, createThread = false) is Conversation.Community -> getThreadIdFor("",null, "${conversation.baseCommunityInfo.baseUrl.removeSuffix("/")}.${conversation.baseCommunityInfo.room}", createThread = false) is Conversation.ClosedGroup -> getThreadIdFor(conversation.sessionId, null, null, createThread = false) // New groups will be managed bia libsession } if (threadId != null) { if (conversation.lastRead > getLastSeen(threadId)) { markConversationAsRead(threadId, conversation.lastRead, force = true) } updateThread(threadId, false) } } } private fun updateUserGroups(userGroups: UserGroupsConfig) { val threadDb = DatabaseComponent.get(context).threadDatabase() val localUserPublicKey = getUserPublicKey() ?: return Log.w( "Loki", "No user public key when trying to update user groups from config" ) val communities = userGroups.allCommunityInfo() val lgc = userGroups.allLegacyGroupInfo() val allOpenGroups = getAllOpenGroups() val toDeleteCommunities = allOpenGroups.filter { Conversation.Community(BaseCommunityInfo(it.value.server, it.value.room, it.value.publicKey), 0, false).baseCommunityInfo.fullUrl() !in communities.map { it.community.fullUrl() } } val existingCommunities: Map = allOpenGroups.filterKeys { it !in toDeleteCommunities.keys } val toAddCommunities = communities.filter { it.community.fullUrl() !in existingCommunities.map { it.value.joinURL } } val existingJoinUrls = existingCommunities.values.map { it.joinURL } val existingClosedGroups = getAllGroups(includeInactive = true).filter { it.isLegacyClosedGroup } val lgcIds = lgc.map { it.sessionId.hexString() } val toDeleteClosedGroups = existingClosedGroups.filter { group -> GroupUtil.doubleDecodeGroupId(group.encodedId) !in lgcIds } // delete the ones which are not listed in the config toDeleteCommunities.values.forEach { openGroup -> OpenGroupManager.delete(openGroup.server, openGroup.room, context) } toDeleteClosedGroups.forEach { deleteGroup -> val threadId = getThreadId(deleteGroup.encodedId) if (threadId != null) { ClosedGroupManager.silentlyRemoveGroup(context,threadId,GroupUtil.doubleDecodeGroupId(deleteGroup.encodedId), deleteGroup.encodedId, localUserPublicKey, delete = true) } } toAddCommunities.forEach { toAddCommunity -> val joinUrl = toAddCommunity.community.fullUrl() if (!hasBackgroundGroupAddJob(joinUrl)) { JobQueue.shared.add(BackgroundGroupAddJob(joinUrl)) } } for (groupInfo in communities) { val groupBaseCommunity = groupInfo.community if (groupBaseCommunity.fullUrl() in existingJoinUrls) { // add it val (threadId, _) = existingCommunities.entries.first { (_, v) -> v.joinURL == groupInfo.community.fullUrl() } threadDb.setPinned(threadId, groupInfo.priority == PRIORITY_PINNED) } } val newClosedGroups = userGroups.allClosedGroupInfo() for (closedGroup in newClosedGroups) { val recipient = Recipient.from(context, Address.fromSerialized(closedGroup.groupSessionId.hexString()), false) setRecipientApprovedMe(recipient, true) setRecipientApproved(recipient, true) val threadId = getOrCreateThreadIdFor(recipient.address) setPinned(threadId, closedGroup.priority == PRIORITY_PINNED) pollerFactory.pollerFor(closedGroup.groupSessionId)?.start() } for (group in lgc) { val existingGroup = existingClosedGroups.firstOrNull { GroupUtil.doubleDecodeGroupId(it.encodedId) == group.sessionId.hexString() } val existingThread = existingGroup?.let { getThreadId(existingGroup.encodedId) } if (existingGroup != null) { if (group.priority == PRIORITY_HIDDEN && existingThread != null) { ClosedGroupManager.silentlyRemoveGroup(context,existingThread,GroupUtil.doubleDecodeGroupId(existingGroup.encodedId), existingGroup.encodedId, localUserPublicKey, delete = true) } else if (existingThread == null) { Log.w("Loki-DBG", "Existing group had no thread to hide") } else { Log.d("Loki-DBG", "Setting existing group pinned status to ${group.priority}") threadDb.setPinned(existingThread, group.priority == PRIORITY_PINNED) } } else { val members = group.members.keys.map { Address.fromSerialized(it) } val admins = group.members.filter { it.value /*admin = true*/ }.keys.map { Address.fromSerialized(it) } val groupId = GroupUtil.doubleEncodeGroupID(group.sessionId.hexString()) val title = group.name val formationTimestamp = (group.joinedAt * 1000L) createGroup(groupId, title, admins + members, null, null, admins, formationTimestamp) setProfileSharing(Address.fromSerialized(groupId), true) // Add the group to the user's set of public keys to poll for addClosedGroupPublicKey(group.sessionId.hexString()) // Store the encryption key pair val keyPair = ECKeyPair(DjbECPublicKey(group.encPubKey), DjbECPrivateKey(group.encSecKey)) addClosedGroupEncryptionKeyPair(keyPair, group.sessionId.hexString(), SnodeAPI.nowWithOffset) // Set expiration timer val expireTimer = group.disappearingTimer setExpirationTimer(groupId, expireTimer.toInt()) // Notify the PN server PushRegistryV1.subscribeGroup(group.sessionId.hexString(), publicKey = localUserPublicKey) // Notify the user val threadID = getOrCreateThreadIdFor(Address.fromSerialized(groupId)) threadDb.setDate(threadID, formationTimestamp) insertOutgoingInfoMessage(context, groupId, SignalServiceGroup.Type.CREATION, title, members.map { it.serialize() }, admins.map { it.serialize() }, threadID, formationTimestamp) // Don't create config group here, it's from a config update // Start polling LegacyClosedGroupPollerV2.shared.startPolling(group.sessionId.hexString()) } } } override fun setAuthToken(room: String, server: String, newValue: String) { val id = "$server.$room" DatabaseComponent.get(context).lokiAPIDatabase().setAuthToken(id, newValue) } override fun removeAuthToken(room: String, server: String) { val id = "$server.$room" DatabaseComponent.get(context).lokiAPIDatabase().setAuthToken(id, null) } override fun getOpenGroup(threadId: Long): OpenGroup? { if (threadId.toInt() < 0) { return null } val database = databaseHelper.readableDatabase return database.get(LokiThreadDatabase.publicChatTable, "${LokiThreadDatabase.threadID} = ?", arrayOf( threadId.toString() )) { cursor -> val publicChatAsJson = cursor.getString(LokiThreadDatabase.publicChat) OpenGroup.fromJSON(publicChatAsJson) } } override fun getOpenGroupPublicKey(server: String): String? { return DatabaseComponent.get(context).lokiAPIDatabase().getOpenGroupPublicKey(server) } override fun setOpenGroupPublicKey(server: String, newValue: String) { DatabaseComponent.get(context).lokiAPIDatabase().setOpenGroupPublicKey(server, newValue) } override fun getLastMessageServerID(room: String, server: String): Long? { return DatabaseComponent.get(context).lokiAPIDatabase().getLastMessageServerID(room, server) } override fun setLastMessageServerID(room: String, server: String, newValue: Long) { DatabaseComponent.get(context).lokiAPIDatabase().setLastMessageServerID(room, server, newValue) } override fun removeLastMessageServerID(room: String, server: String) { DatabaseComponent.get(context).lokiAPIDatabase().removeLastMessageServerID(room, server) } override fun getLastDeletionServerID(room: String, server: String): Long? { return DatabaseComponent.get(context).lokiAPIDatabase().getLastDeletionServerID(room, server) } override fun setLastDeletionServerID(room: String, server: String, newValue: Long) { DatabaseComponent.get(context).lokiAPIDatabase().setLastDeletionServerID(room, server, newValue) } override fun removeLastDeletionServerID(room: String, server: String) { DatabaseComponent.get(context).lokiAPIDatabase().removeLastDeletionServerID(room, server) } override fun setUserCount(room: String, server: String, newValue: Int) { DatabaseComponent.get(context).lokiAPIDatabase().setUserCount(room, server, newValue) } override fun setOpenGroupServerMessageID(messageID: Long, serverID: Long, threadID: Long, isSms: Boolean) { DatabaseComponent.get(context).lokiMessageDatabase().setServerID(messageID, serverID, isSms) DatabaseComponent.get(context).lokiMessageDatabase().setOriginalThreadID(messageID, serverID, threadID) } override fun getOpenGroup(room: String, server: String): OpenGroup? { return getAllOpenGroups().values.firstOrNull { it.server == server && it.room == room } } override fun setGroupMemberRoles(members: List) { DatabaseComponent.get(context).groupMemberDatabase().setGroupMembers(members) } override fun isDuplicateMessage(timestamp: Long): Boolean { return getReceivedMessageTimestamps().contains(timestamp) } override fun updateTitle(groupID: String, newValue: String) { DatabaseComponent.get(context).groupDatabase().updateTitle(groupID, newValue) } override fun updateProfilePicture(groupID: String, newValue: ByteArray) { DatabaseComponent.get(context).groupDatabase().updateProfilePicture(groupID, newValue) } override fun removeProfilePicture(groupID: String) { DatabaseComponent.get(context).groupDatabase().removeProfilePicture(groupID) } override fun hasDownloadedProfilePicture(groupID: String): Boolean { return DatabaseComponent.get(context).groupDatabase().hasDownloadedProfilePicture(groupID) } override fun getReceivedMessageTimestamps(): Set { return SessionMetaProtocol.getTimestamps() } override fun addReceivedMessageTimestamp(timestamp: Long) { SessionMetaProtocol.addTimestamp(timestamp) } override fun removeReceivedMessageTimestamps(timestamps: Set) { SessionMetaProtocol.removeTimestamps(timestamps) } override fun getMessageIdInDatabase(timestamp: Long, author: String): Long? { val database = DatabaseComponent.get(context).mmsSmsDatabase() val address = fromSerialized(author) return database.getMessageFor(timestamp, address)?.getId() } override fun updateSentTimestamp( messageID: Long, isMms: Boolean, openGroupSentTimestamp: Long, threadId: Long ) { if (isMms) { val mmsDb = DatabaseComponent.get(context).mmsDatabase() mmsDb.updateSentTimestamp(messageID, openGroupSentTimestamp, threadId) } else { val smsDb = DatabaseComponent.get(context).smsDatabase() smsDb.updateSentTimestamp(messageID, openGroupSentTimestamp, threadId) } } override fun markAsSent(timestamp: Long, author: String) { val database = DatabaseComponent.get(context).mmsSmsDatabase() val messageRecord = database.getMessageFor(timestamp, author) ?: return if (messageRecord.isMms) { val mmsDatabase = DatabaseComponent.get(context).mmsDatabase() mmsDatabase.markAsSent(messageRecord.getId(), true) } else { val smsDatabase = DatabaseComponent.get(context).smsDatabase() smsDatabase.markAsSent(messageRecord.getId(), true) } } override fun markAsSyncing(timestamp: Long, author: String) { DatabaseComponent.get(context).mmsSmsDatabase() .getMessageFor(timestamp, author) ?.run { getMmsDatabaseElseSms(isMms).markAsSyncing(id) } } private fun getMmsDatabaseElseSms(isMms: Boolean) = if (isMms) DatabaseComponent.get(context).mmsDatabase() else DatabaseComponent.get(context).smsDatabase() override fun markAsResyncing(timestamp: Long, author: String) { DatabaseComponent.get(context).mmsSmsDatabase() .getMessageFor(timestamp, author) ?.run { getMmsDatabaseElseSms(isMms).markAsResyncing(id) } } override fun markAsSending(timestamp: Long, author: String) { val database = DatabaseComponent.get(context).mmsSmsDatabase() val messageRecord = database.getMessageFor(timestamp, author) ?: return if (messageRecord.isMms) { val mmsDatabase = DatabaseComponent.get(context).mmsDatabase() mmsDatabase.markAsSending(messageRecord.getId()) } else { val smsDatabase = DatabaseComponent.get(context).smsDatabase() smsDatabase.markAsSending(messageRecord.getId()) messageRecord.isPending } } override fun markUnidentified(timestamp: Long, author: String) { val database = DatabaseComponent.get(context).mmsSmsDatabase() val messageRecord = database.getMessageFor(timestamp, author) ?: return if (messageRecord.isMms) { val mmsDatabase = DatabaseComponent.get(context).mmsDatabase() mmsDatabase.markUnidentified(messageRecord.getId(), true) } else { val smsDatabase = DatabaseComponent.get(context).smsDatabase() smsDatabase.markUnidentified(messageRecord.getId(), true) } } override fun markAsSentFailed(timestamp: Long, author: String, error: Exception) { val database = DatabaseComponent.get(context).mmsSmsDatabase() val messageRecord = database.getMessageFor(timestamp, author) ?: return if (messageRecord.isMms) { val mmsDatabase = DatabaseComponent.get(context).mmsDatabase() mmsDatabase.markAsSentFailed(messageRecord.getId()) } else { val smsDatabase = DatabaseComponent.get(context).smsDatabase() smsDatabase.markAsSentFailed(messageRecord.getId()) } if (error.localizedMessage != null) { val message: String if (error is OnionRequestAPI.HTTPRequestFailedAtDestinationException && error.statusCode == 429) { message = "429: Rate limited." } else { message = error.localizedMessage!! } DatabaseComponent.get(context).lokiMessageDatabase().setErrorMessage(messageRecord.getId(), message) } else { DatabaseComponent.get(context).lokiMessageDatabase().setErrorMessage(messageRecord.getId(), error.javaClass.simpleName) } } override fun markAsSyncFailed(timestamp: Long, author: String, error: Exception) { val database = DatabaseComponent.get(context).mmsSmsDatabase() val messageRecord = database.getMessageFor(timestamp, author) ?: return database.getMessageFor(timestamp, author) ?.run { getMmsDatabaseElseSms(isMms).markAsSyncFailed(id) } if (error.localizedMessage != null) { val message: String if (error is OnionRequestAPI.HTTPRequestFailedAtDestinationException && error.statusCode == 429) { message = "429: Rate limited." } else { message = error.localizedMessage!! } DatabaseComponent.get(context).lokiMessageDatabase().setErrorMessage(messageRecord.getId(), message) } else { DatabaseComponent.get(context).lokiMessageDatabase().setErrorMessage(messageRecord.getId(), error.javaClass.simpleName) } } override fun clearErrorMessage(messageID: Long) { val db = DatabaseComponent.get(context).lokiMessageDatabase() db.clearErrorMessage(messageID) } override fun setMessageServerHash(messageID: Long, serverHash: String) { DatabaseComponent.get(context).lokiMessageDatabase().setMessageServerHash(messageID, serverHash) } override fun getGroup(groupID: String): GroupRecord? { val group = DatabaseComponent.get(context).groupDatabase().getGroup(groupID) return if (group.isPresent) { group.get() } else null } override fun createGroup(groupId: String, title: String?, members: List
, avatar: SignalServiceAttachmentPointer?, relay: String?, admins: List
, formationTimestamp: Long) { DatabaseComponent.get(context).groupDatabase().create(groupId, title, members, avatar, relay, admins, formationTimestamp) } override fun createNewGroup(groupName: String, groupDescription: String, members: Set): Optional { val userGroups = configFactory.userGroups ?: return Optional.absent() val convoVolatile = configFactory.convoVolatile ?: return Optional.absent() val ourSessionId = getUserPublicKey() ?: return Optional.absent() val groupCreationTimestamp = SnodeAPI.nowWithOffset val group = userGroups.createGroup() val adminKey = group.adminKey userGroups.set(group) val groupInfo = configFactory.getGroupInfoConfig(group.groupSessionId) ?: return Optional.absent() val groupMembers = configFactory.getGroupMemberConfig(group.groupSessionId) ?: return Optional.absent() with (groupInfo) { setName(groupName) setDescription(groupDescription) } groupMembers.set( LibSessionGroupMember(ourSessionId, getUserProfile().displayName, admin = true) ) members.forEach { groupMembers.set(LibSessionGroupMember(it.sessionID, it.name, invitePending = true)) } val groupKeys = configFactory.constructGroupKeysConfig(group.groupSessionId, info = groupInfo, members = groupMembers) ?: return Optional.absent() val newGroupRecipient = group.groupSessionId.hexString() val configTtl = 1 * 24 * 60 * 60 * 1000L // TODO: just testing here, 1 day so we don't fill large space on network // Test the sending val keyPush = groupKeys.pendingConfig() ?: return Optional.absent() val keysSnodeMessage = SnodeMessage( newGroupRecipient, Base64.encodeBytes(keyPush), configTtl, groupCreationTimestamp ) val keysBatchInfo = SnodeAPI.buildAuthenticatedStoreBatchInfo( groupKeys.namespace(), keysSnodeMessage, adminKey ) val (infoPush, infoSeqNo) = groupInfo.push() val infoSnodeMessage = SnodeMessage( newGroupRecipient, Base64.encodeBytes(infoPush), configTtl, groupCreationTimestamp ) val infoBatchInfo = SnodeAPI.buildAuthenticatedStoreBatchInfo( groupInfo.namespace(), infoSnodeMessage, adminKey ) val (memberPush, memberSeqNo) = groupMembers.push() val memberSnodeMessage = SnodeMessage( newGroupRecipient, Base64.encodeBytes(memberPush), configTtl, groupCreationTimestamp ) val memberBatchInfo = SnodeAPI.buildAuthenticatedStoreBatchInfo( groupMembers.namespace(), memberSnodeMessage, adminKey ) try { val snode = SnodeAPI.getSingleTargetSnode(newGroupRecipient).get() val response = SnodeAPI.getRawBatchResponse( snode, newGroupRecipient, listOf(keysBatchInfo, infoBatchInfo, memberBatchInfo), true ).get() @Suppress("UNCHECKED_CAST") val responseList = (response["results"] as List) val keyResponse = responseList[0] val keyHash = (keyResponse["body"] as Map)["hash"] as String val keyTimestamp = (keyResponse["body"] as Map)["t"] as Long val infoResponse = responseList[1] val infoHash = (infoResponse["body"] as Map)["hash"] as String val memberResponse = responseList[2] val memberHash = (memberResponse["body"] as Map)["hash"] as String // TODO: check response success groupKeys.loadKey(keyPush, keyHash, keyTimestamp, groupInfo, groupMembers) groupInfo.confirmPushed(infoSeqNo, infoHash) groupMembers.confirmPushed(memberSeqNo, memberHash) configFactory.saveGroupConfigs(groupKeys, groupInfo, groupMembers) // now check poller to be all convoVolatile.set(Conversation.ClosedGroup(newGroupRecipient, groupCreationTimestamp, false)) ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context) Log.d("Group Config", "Saved group config for $newGroupRecipient") groupKeys.free() groupInfo.free() groupMembers.free() val groupRecipient = Recipient.from(context, Address.fromSerialized(newGroupRecipient), false) setRecipientApprovedMe(groupRecipient, true) setRecipientApproved(groupRecipient, true) pollerFactory.updatePollers() members.forEach { contact -> val job = InviteContactJob(group.groupSessionId.hexString(), contact.sessionID) JobQueue.shared.add(job) } return Optional.of(groupRecipient) } catch (e: Exception) { Log.e("Group Config", e) Log.e("Group Config", "Deleting group from our group") // delete the group from user groups userGroups.erase(group) } return Optional.absent() } override fun createInitialConfigGroup(groupPublicKey: String, name: String, members: Map, formationTimestamp: Long, encryptionKeyPair: ECKeyPair) { val volatiles = configFactory.convoVolatile ?: return val userGroups = configFactory.userGroups ?: return val groupVolatileConfig = volatiles.getOrConstructLegacyGroup(groupPublicKey) groupVolatileConfig.lastRead = formationTimestamp volatiles.set(groupVolatileConfig) val groupInfo = GroupInfo.LegacyGroupInfo( sessionId = SessionId.from(groupPublicKey), name = name, members = members, priority = PRIORITY_VISIBLE, encPubKey = (encryptionKeyPair.publicKey as DjbECPublicKey).publicKey, // 'serialize()' inserts an extra byte encSecKey = encryptionKeyPair.privateKey.serialize(), disappearingTimer = 0L, joinedAt = (formationTimestamp / 1000L) ) // shouldn't exist, don't use getOrConstruct + copy userGroups.set(groupInfo) ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context) } override fun updateGroupConfig(groupPublicKey: String) { val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey) val groupAddress = fromSerialized(groupID) // TODO: probably add a check in here for isActive? // TODO: also check if local user is a member / maybe run delete otherwise? val existingGroup = getGroup(groupID) ?: return Log.w("Loki-DBG", "No existing group for ${groupPublicKey.take(4)}} when updating group config") val userGroups = configFactory.userGroups ?: return if (!existingGroup.isActive) { userGroups.eraseLegacyGroup(groupPublicKey) return } val name = existingGroup.title val admins = existingGroup.admins.map { it.serialize() } val members = existingGroup.members.map { it.serialize() } val membersMap = GroupUtil.createConfigMemberMap(admins = admins, members = members) val latestKeyPair = getLatestClosedGroupEncryptionKeyPair(groupPublicKey) ?: return Log.w("Loki-DBG", "No latest closed group encryption key pair for ${groupPublicKey.take(4)}} when updating group config") val recipientSettings = getRecipientSettings(groupAddress) ?: return val threadID = getThreadId(groupAddress) ?: return val groupInfo = userGroups.getOrConstructLegacyGroupInfo(groupPublicKey).copy( name = name, members = membersMap, encPubKey = (latestKeyPair.publicKey as DjbECPublicKey).publicKey, // 'serialize()' inserts an extra byte encSecKey = latestKeyPair.privateKey.serialize(), priority = if (isPinned(threadID)) PRIORITY_PINNED else PRIORITY_VISIBLE, disappearingTimer = recipientSettings.expireMessages.toLong(), joinedAt = (existingGroup.formationTimestamp / 1000L) ) userGroups.set(groupInfo) } override fun isGroupActive(groupPublicKey: String): Boolean { return DatabaseComponent.get(context).groupDatabase().getGroup(GroupUtil.doubleEncodeGroupID(groupPublicKey)).orNull()?.isActive == true } override fun setActive(groupID: String, value: Boolean) { DatabaseComponent.get(context).groupDatabase().setActive(groupID, value) } override fun getZombieMembers(groupID: String): Set { return DatabaseComponent.get(context).groupDatabase().getGroupZombieMembers(groupID).map { it.address.serialize() }.toHashSet() } override fun removeMember(groupID: String, member: Address) { DatabaseComponent.get(context).groupDatabase().removeMember(groupID, member) } override fun updateMembers(groupID: String, members: List
) { DatabaseComponent.get(context).groupDatabase().updateMembers(groupID, members) } override fun setZombieMembers(groupID: String, members: List
) { DatabaseComponent.get(context).groupDatabase().updateZombieMembers(groupID, members) } override fun insertIncomingInfoMessage(context: Context, senderPublicKey: String, groupID: String, type: SignalServiceGroup.Type, name: String, members: Collection, admins: Collection, sentTimestamp: Long) { val group = SignalServiceGroup(type, GroupUtil.getDecodedGroupIDAsData(groupID), SignalServiceGroup.GroupType.SIGNAL, name, members.toList(), null, admins.toList()) val m = IncomingTextMessage(fromSerialized(senderPublicKey), 1, sentTimestamp, "", Optional.of(group), 0, true, false) val updateData = UpdateMessageData.buildGroupUpdate(type, name, members)?.toJSON() val infoMessage = IncomingGroupMessage(m, groupID, updateData, true) val smsDB = DatabaseComponent.get(context).smsDatabase() smsDB.insertMessageInbox(infoMessage, true) } override fun insertOutgoingInfoMessage(context: Context, groupID: String, type: SignalServiceGroup.Type, name: String, members: Collection, admins: Collection, threadID: Long, sentTimestamp: Long) { val userPublicKey = getUserPublicKey() val recipient = Recipient.from(context, fromSerialized(groupID), false) val updateData = UpdateMessageData.buildGroupUpdate(type, name, members)?.toJSON() ?: "" val infoMessage = OutgoingGroupMediaMessage(recipient, updateData, groupID, null, sentTimestamp, 0, true, null, listOf(), listOf()) val mmsDB = DatabaseComponent.get(context).mmsDatabase() val mmsSmsDB = DatabaseComponent.get(context).mmsSmsDatabase() if (mmsSmsDB.getMessageFor(sentTimestamp, userPublicKey) != null) return val infoMessageID = mmsDB.insertMessageOutbox(infoMessage, threadID, false, null, runThreadUpdate = true) mmsDB.markAsSent(infoMessageID, true) } override fun isLegacyClosedGroup(publicKey: String): Boolean { return DatabaseComponent.get(context).lokiAPIDatabase().isClosedGroup(publicKey) } override fun getClosedGroupEncryptionKeyPairs(groupPublicKey: String): MutableList { return DatabaseComponent.get(context).lokiAPIDatabase().getClosedGroupEncryptionKeyPairs(groupPublicKey).toMutableList() } override fun getLatestClosedGroupEncryptionKeyPair(groupPublicKey: String): ECKeyPair? { return DatabaseComponent.get(context).lokiAPIDatabase().getLatestClosedGroupEncryptionKeyPair(groupPublicKey) } override fun getAllClosedGroupPublicKeys(): Set { return DatabaseComponent.get(context).lokiAPIDatabase().getAllClosedGroupPublicKeys() } override fun getAllActiveClosedGroupPublicKeys(): Set { return DatabaseComponent.get(context).lokiAPIDatabase().getAllClosedGroupPublicKeys().filter { getGroup(GroupUtil.doubleEncodeGroupID(it))?.isActive == true }.toSet() } override fun addClosedGroupPublicKey(groupPublicKey: String) { DatabaseComponent.get(context).lokiAPIDatabase().addClosedGroupPublicKey(groupPublicKey) } override fun removeClosedGroupPublicKey(groupPublicKey: String) { DatabaseComponent.get(context).lokiAPIDatabase().removeClosedGroupPublicKey(groupPublicKey) } override fun addClosedGroupEncryptionKeyPair(encryptionKeyPair: ECKeyPair, groupPublicKey: String, timestamp: Long) { DatabaseComponent.get(context).lokiAPIDatabase().addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey, timestamp) } override fun removeAllClosedGroupEncryptionKeyPairs(groupPublicKey: String) { DatabaseComponent.get(context).lokiAPIDatabase().removeAllClosedGroupEncryptionKeyPairs(groupPublicKey) } override fun updateFormationTimestamp(groupID: String, formationTimestamp: Long) { DatabaseComponent.get(context).groupDatabase() .updateFormationTimestamp(groupID, formationTimestamp) } override fun updateTimestampUpdated(groupID: String, updatedTimestamp: Long) { DatabaseComponent.get(context).groupDatabase() .updateTimestampUpdated(groupID, updatedTimestamp) } override fun setExpirationTimer(address: String, duration: Int) { val recipient = Recipient.from(context, fromSerialized(address), false) DatabaseComponent.get(context).recipientDatabase().setExpireMessages(recipient, duration) if (recipient.isContactRecipient && !recipient.isLocalNumber) { configFactory.contacts?.upsertContact(address) { this.expiryMode = if (duration != 0) { ExpiryMode.AfterRead(duration.toLong()) } else { // = 0 / delete ExpiryMode.NONE } } if (configFactory.contacts?.needsPush() == true) { ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context) } } } override fun getMembers(groupPublicKey: String): List = configFactory.getGroupMemberConfig(SessionId.from(groupPublicKey))?.use { it.all() }?.toList() ?: emptyList() override fun acceptClosedGroupInvite(groupId: SessionId, name: String, authData: ByteArray, invitingAdmin: SessionId) { val recipient = Recipient.from(context, fromSerialized(groupId.hexString()), false) val profileManager = SSKEnvironment.shared.profileManager val groups = configFactory.userGroups ?: return val closedGroupInfo = GroupInfo.ClosedGroupInfo( groupId, byteArrayOf(), authData, PRIORITY_VISIBLE ) groups.set(closedGroupInfo) configFactory.persist(groups, SnodeAPI.nowWithOffset) profileManager.setName(context, recipient, name) setRecipientApprovedMe(recipient, true) setRecipientApproved(recipient, true) getOrCreateThreadIdFor(recipient.address) pollerFactory.pollerFor(groupId)?.start() val inviteResponse = GroupUpdateInviteResponseMessage.newBuilder() .setIsApproved(true) val responseData = DataMessage.GroupUpdateMessage.newBuilder() .setInviteResponse(inviteResponse) val responseMessage = GroupUpdated(responseData.build()) // this will fail the first couple of times :) MessageSender.send(responseMessage, fromSerialized(groupId.hexString())) } override fun setGroupInviteCompleteIfNeeded(approved: Boolean, invitee: String, closedGroup: SessionId) { // don't try to process invitee acceptance if we aren't admin if (configFactory.userGroups?.getClosedGroup(closedGroup.hexString())?.hasAdminKey() != true) return configFactory.getGroupMemberConfig(closedGroup)?.use { groupMembers -> val member = groupMembers.get(invitee) ?: run { Log.e("ClosedGroup", "User wasn't in the group membership to add!") return } if (!member.invitePending) return groupMembers.close() if (approved) { groupMembers.set(member.copy(invitePending = false)) } else { groupMembers.erase(member) } configFactory.persistGroupConfigDump(groupMembers, closedGroup, SnodeAPI.nowWithOffset) ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(Destination.ClosedGroup(closedGroup.hexString())) } } override fun setServerCapabilities(server: String, capabilities: List) { return DatabaseComponent.get(context).lokiAPIDatabase().setServerCapabilities(server, capabilities) } override fun getServerCapabilities(server: String): List { return DatabaseComponent.get(context).lokiAPIDatabase().getServerCapabilities(server) } override fun getAllOpenGroups(): Map { return DatabaseComponent.get(context).lokiThreadDatabase().getAllOpenGroups() } override fun updateOpenGroup(openGroup: OpenGroup) { OpenGroupManager.updateOpenGroup(openGroup, context) } override fun getAllGroups(includeInactive: Boolean): List { return DatabaseComponent.get(context).groupDatabase().getAllGroups(includeInactive) } override fun addOpenGroup(urlAsString: String): OpenGroupApi.RoomInfo? { return OpenGroupManager.addOpenGroup(urlAsString, context) } override fun onOpenGroupAdded(server: String, room: String) { OpenGroupManager.restartPollerForServer(server.removeSuffix("/")) val groups = configFactory.userGroups ?: return val volatileConfig = configFactory.convoVolatile ?: return val openGroup = getOpenGroup(room, server) ?: return val (infoServer, infoRoom, pubKey) = BaseCommunityInfo.parseFullUrl(openGroup.joinURL) ?: return val pubKeyHex = Hex.toStringCondensed(pubKey) val communityInfo = groups.getOrConstructCommunityInfo(infoServer, infoRoom, pubKeyHex) groups.set(communityInfo) val volatile = volatileConfig.getOrConstructCommunity(infoServer, infoRoom, pubKey) if (volatile.lastRead != 0L) { val threadId = getThreadId(openGroup) ?: return markConversationAsRead(threadId, volatile.lastRead, force = true) } volatileConfig.set(volatile) } override fun hasBackgroundGroupAddJob(groupJoinUrl: String): Boolean { val jobDb = DatabaseComponent.get(context).sessionJobDatabase() return jobDb.hasBackgroundGroupAddJob(groupJoinUrl) } override fun setProfileSharing(address: Address, value: Boolean) { val recipient = Recipient.from(context, address, false) DatabaseComponent.get(context).recipientDatabase().setProfileSharing(recipient, value) } override fun getOrCreateThreadIdFor(address: Address): Long { val recipient = Recipient.from(context, address, false) return DatabaseComponent.get(context).threadDatabase().getOrCreateThreadIdFor(recipient) } override fun getThreadIdFor(publicKey: String, groupPublicKey: String?, openGroupID: String?, createThread: Boolean): Long? { val database = DatabaseComponent.get(context).threadDatabase() return if (!openGroupID.isNullOrEmpty()) { val recipient = Recipient.from(context, fromSerialized(GroupUtil.getEncodedOpenGroupID(openGroupID.toByteArray())), false) database.getThreadIdIfExistsFor(recipient).let { if (it == -1L) null else it } } else if (!groupPublicKey.isNullOrEmpty() && !groupPublicKey.startsWith(IdPrefix.GROUP.value)) { val recipient = Recipient.from(context, fromSerialized(GroupUtil.doubleEncodeGroupID(groupPublicKey)), false) if (createThread) database.getOrCreateThreadIdFor(recipient) else database.getThreadIdIfExistsFor(recipient).let { if (it == -1L) null else it } } else if (!groupPublicKey.isNullOrEmpty()) { val recipient = Recipient.from(context, fromSerialized(groupPublicKey), false) if (createThread) database.getOrCreateThreadIdFor(recipient) else database.getThreadIdIfExistsFor(recipient).let { if (it == -1L) null else it } } else { val recipient = Recipient.from(context, fromSerialized(publicKey), false) if (createThread) database.getOrCreateThreadIdFor(recipient) else database.getThreadIdIfExistsFor(recipient).let { if (it == -1L) null else it } } } override fun getThreadId(publicKeyOrOpenGroupID: String): Long? { val address = fromSerialized(publicKeyOrOpenGroupID) return getThreadId(address) } override fun getThreadId(openGroup: OpenGroup): Long? { return GroupManager.getOpenGroupThreadID("${openGroup.server.removeSuffix("/")}.${openGroup.room}", context) } override fun getThreadId(address: Address): Long? { val recipient = Recipient.from(context, address, false) return getThreadId(recipient) } override fun getThreadId(recipient: Recipient): Long? { val threadID = DatabaseComponent.get(context).threadDatabase().getThreadIdIfExistsFor(recipient) return if (threadID < 0) null else threadID } override fun getThreadIdForMms(mmsId: Long): Long { val mmsDb = DatabaseComponent.get(context).mmsDatabase() val cursor = mmsDb.getMessage(mmsId) val reader = mmsDb.readerFor(cursor) val threadId = reader.next?.threadId cursor.close() return threadId ?: -1 } override fun getContactWithSessionID(sessionID: String): Contact? { return DatabaseComponent.get(context).sessionContactDatabase().getContactWithSessionID(sessionID) } override fun getAllContacts(): Set { return DatabaseComponent.get(context).sessionContactDatabase().getAllContacts() } override fun setContact(contact: Contact) { DatabaseComponent.get(context).sessionContactDatabase().setContact(contact) val address = fromSerialized(contact.sessionID) if (!getRecipientApproved(address)) return val recipientHash = SSKEnvironment.shared.profileManager.contactUpdatedInternal(contact) val recipient = Recipient.from(context, address, false) setRecipientHash(recipient, recipientHash) } override fun getRecipientForThread(threadId: Long): Recipient? { return DatabaseComponent.get(context).threadDatabase().getRecipientForThreadId(threadId) } override fun getRecipientSettings(address: Address): Recipient.RecipientSettings? { val recipientSettings = DatabaseComponent.get(context).recipientDatabase().getRecipientSettings(address) return if (recipientSettings.isPresent) { recipientSettings.get() } else null } override fun hasAutoDownloadFlagBeenSet(recipient: Recipient): Boolean { return DatabaseComponent.get(context).recipientDatabase().isAutoDownloadFlagSet(recipient) } override fun addLibSessionContacts(contacts: List) { val mappingDb = DatabaseComponent.get(context).blindedIdMappingDatabase() val moreContacts = contacts.filter { contact -> val id = SessionId(contact.id) id.prefix?.isBlinded() == false || mappingDb.getBlindedIdMapping(contact.id).none { it.sessionId != null } } val profileManager = SSKEnvironment.shared.profileManager moreContacts.forEach { contact -> val address = fromSerialized(contact.id) val recipient = Recipient.from(context, address, false) setBlocked(listOf(recipient), contact.blocked, fromConfigUpdate = true) setRecipientApproved(recipient, contact.approved) setRecipientApprovedMe(recipient, contact.approvedMe) if (contact.name.isNotEmpty()) { profileManager.setName(context, recipient, contact.name) } else { profileManager.setName(context, recipient, null) } if (contact.nickname.isNotEmpty()) { profileManager.setNickname(context, recipient, contact.nickname) } else { profileManager.setNickname(context, recipient, null) } if (contact.profilePicture != UserPic.DEFAULT) { val (url, key) = contact.profilePicture if (key.size != ProfileKeyUtil.PROFILE_KEY_BYTES) return@forEach profileManager.setProfilePicture(context, recipient, url, key) profileManager.setUnidentifiedAccessMode(context, recipient, Recipient.UnidentifiedAccessMode.UNKNOWN) } else { profileManager.setProfilePicture(context, recipient, null, null) } if (contact.priority == PRIORITY_HIDDEN) { getThreadId(fromSerialized(contact.id))?.let { conversationThreadId -> deleteConversation(conversationThreadId) } } else { getThreadId(fromSerialized(contact.id))?.let { conversationThreadId -> setPinned(conversationThreadId, contact.priority == PRIORITY_PINNED) } } setRecipientHash(recipient, contact.hashCode().toString()) } } override fun addContacts(contacts: List) { val recipientDatabase = DatabaseComponent.get(context).recipientDatabase() val threadDatabase = DatabaseComponent.get(context).threadDatabase() val mappingDb = DatabaseComponent.get(context).blindedIdMappingDatabase() val moreContacts = contacts.filter { contact -> val id = SessionId(contact.publicKey) id.prefix != IdPrefix.BLINDED || mappingDb.getBlindedIdMapping(contact.publicKey).none { it.sessionId != null } } for (contact in moreContacts) { val address = fromSerialized(contact.publicKey) val recipient = Recipient.from(context, address, true) if (!contact.profilePicture.isNullOrEmpty()) { recipientDatabase.setProfileAvatar(recipient, contact.profilePicture) } if (contact.profileKey?.isNotEmpty() == true) { recipientDatabase.setProfileKey(recipient, contact.profileKey) } if (contact.name.isNotEmpty()) { recipientDatabase.setProfileName(recipient, contact.name) } recipientDatabase.setProfileSharing(recipient, true) recipientDatabase.setRegistered(recipient, Recipient.RegisteredState.REGISTERED) // create Thread if needed val threadId = threadDatabase.getThreadIdIfExistsFor(recipient) if (contact.didApproveMe == true) { recipientDatabase.setApprovedMe(recipient, true) } if (contact.isApproved == true && threadId != -1L) { setRecipientApproved(recipient, true) threadDatabase.setHasSent(threadId, true) } val contactIsBlocked: Boolean? = contact.isBlocked if (contactIsBlocked != null && recipient.isBlocked != contactIsBlocked) { setBlocked(listOf(recipient), contactIsBlocked, fromConfigUpdate = true) } } if (contacts.isNotEmpty()) { threadDatabase.notifyConversationListListeners() } } override fun shouldAutoDownloadAttachments(recipient: Recipient): Boolean { return recipient.autoDownloadAttachments } override fun setAutoDownloadAttachments( recipient: Recipient, shouldAutoDownloadAttachments: Boolean ) { val recipientDb = DatabaseComponent.get(context).recipientDatabase() recipientDb.setAutoDownloadAttachments(recipient, shouldAutoDownloadAttachments) } override fun setRecipientHash(recipient: Recipient, recipientHash: String?) { val recipientDb = DatabaseComponent.get(context).recipientDatabase() recipientDb.setRecipientHash(recipient, recipientHash) } override fun getLastUpdated(threadID: Long): Long { val threadDB = DatabaseComponent.get(context).threadDatabase() return threadDB.getLastUpdated(threadID) } override fun trimThread(threadID: Long, threadLimit: Int) { val threadDB = DatabaseComponent.get(context).threadDatabase() threadDB.trimThread(threadID, threadLimit) } override fun trimThreadBefore(threadID: Long, timestamp: Long) { val threadDB = DatabaseComponent.get(context).threadDatabase() threadDB.trimThreadBefore(threadID, timestamp) } override fun getMessageCount(threadID: Long): Long { val mmsSmsDb = DatabaseComponent.get(context).mmsSmsDatabase() return mmsSmsDb.getConversationCount(threadID) } override fun setPinned(threadID: Long, isPinned: Boolean) { val threadDB = DatabaseComponent.get(context).threadDatabase() threadDB.setPinned(threadID, isPinned) val threadRecipient = getRecipientForThread(threadID) ?: return if (threadRecipient.isLocalNumber) { val user = configFactory.user ?: return user.setNtsPriority(if (isPinned) PRIORITY_PINNED else PRIORITY_VISIBLE) } else if (threadRecipient.isContactRecipient) { val contacts = configFactory.contacts ?: return contacts.upsertContact(threadRecipient.address.serialize()) { priority = if (isPinned) PRIORITY_PINNED else PRIORITY_VISIBLE } } else if (threadRecipient.isGroupRecipient) { val groups = configFactory.userGroups ?: return when { threadRecipient.isLegacyClosedGroupRecipient -> { val sessionId = GroupUtil.doubleDecodeGroupId(threadRecipient.address.serialize()) val newGroupInfo = groups.getOrConstructLegacyGroupInfo(sessionId).copy ( priority = if (isPinned) PRIORITY_PINNED else PRIORITY_VISIBLE ) groups.set(newGroupInfo) } threadRecipient.isClosedGroupRecipient -> { val newGroupInfo = groups.getOrConstructClosedGroup(threadRecipient.address.serialize()).copy ( priority = if (isPinned) PRIORITY_PINNED else PRIORITY_VISIBLE ) groups.set(newGroupInfo) } threadRecipient.isOpenGroupRecipient -> { val openGroup = getOpenGroup(threadID) ?: return val (baseUrl, room, pubKeyHex) = BaseCommunityInfo.parseFullUrl(openGroup.joinURL) ?: return val newGroupInfo = groups.getOrConstructCommunityInfo(baseUrl, room, Hex.toStringCondensed(pubKeyHex)).copy ( priority = if (isPinned) PRIORITY_PINNED else PRIORITY_VISIBLE ) groups.set(newGroupInfo) } } } ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context) } override fun isPinned(threadID: Long): Boolean { val threadDB = DatabaseComponent.get(context).threadDatabase() return threadDB.isPinned(threadID) } override fun setThreadDate(threadId: Long, newDate: Long) { val threadDb = DatabaseComponent.get(context).threadDatabase() threadDb.setDate(threadId, newDate) } override fun deleteConversation(threadID: Long) { val recipient = getRecipientForThread(threadID) val threadDB = DatabaseComponent.get(context).threadDatabase() val groupDB = DatabaseComponent.get(context).groupDatabase() threadDB.deleteConversation(threadID) if (recipient != null) { if (recipient.isContactRecipient) { if (recipient.isLocalNumber) return val contacts = configFactory.contacts ?: return contacts.upsertContact(recipient.address.serialize()) { this.priority = PRIORITY_HIDDEN } ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context) } else if (recipient.isClosedGroupRecipient) { // TODO: handle closed group val volatile = configFactory.convoVolatile ?: return val groups = configFactory.userGroups ?: return val groupID = recipient.address.toGroupString() val closedGroup = getGroup(groupID) val groupPublicKey = GroupUtil.doubleDecodeGroupId(recipient.address.serialize()) if (closedGroup != null) { groupDB.delete(groupID) // TODO: Should we delete the group? (seems odd to leave it) volatile.eraseLegacyClosedGroup(groupPublicKey) groups.eraseLegacyGroup(groupPublicKey) } else { Log.w("Loki-DBG", "Failed to find a closed group for ${groupPublicKey.take(4)}") } } } } override fun clearMessages(threadID: Long, fromUser: Address?): Boolean { val smsDb = DatabaseComponent.get(context).smsDatabase() val mmsDb = DatabaseComponent.get(context).mmsDatabase() val threadDb = DatabaseComponent.get(context).threadDatabase() if (fromUser == null) { smsDb.deleteThread(threadID) mmsDb.deleteThread(threadID) // threadDB update called from within } else { smsDb.deleteMessagesFrom(threadID, fromUser.serialize()) mmsDb.deleteMessagesFrom(threadID, fromUser.serialize()) threadDb.update(threadID, false, true) } return true } override fun clearMedia(threadID: Long, fromUser: Address?): Boolean { val mmsDb = DatabaseComponent.get(context).mmsDatabase() mmsDb.deleteMediaFor(threadID, fromUser?.serialize()) return true } override fun getAttachmentDataUri(attachmentId: AttachmentId): Uri { return PartAuthority.getAttachmentDataUri(attachmentId) } override fun getAttachmentThumbnailUri(attachmentId: AttachmentId): Uri { return PartAuthority.getAttachmentThumbnailUri(attachmentId) } override fun insertDataExtractionNotificationMessage(senderPublicKey: String, message: DataExtractionNotificationInfoMessage, sentTimestamp: Long) { val database = DatabaseComponent.get(context).mmsDatabase() val address = fromSerialized(senderPublicKey) val recipient = Recipient.from(context, address, false) if (recipient.isBlocked) return val threadId = getThreadId(recipient) ?: return val mediaMessage = IncomingMediaMessage( address, sentTimestamp, -1, 0, false, false, false, false, Optional.absent(), Optional.absent(), Optional.absent(), Optional.absent(), Optional.absent(), Optional.absent(), Optional.of(message) ) database.insertSecureDecryptedMessageInbox(mediaMessage, threadId, runThreadUpdate = true) } override fun insertMessageRequestResponse(response: MessageRequestResponse) { val userPublicKey = getUserPublicKey() val senderPublicKey = response.sender!! val recipientPublicKey = response.recipient!! if ( userPublicKey == null || (userPublicKey != recipientPublicKey && userPublicKey != senderPublicKey) // this is true if it is a sync message || (userPublicKey == recipientPublicKey && userPublicKey == senderPublicKey) ) return val recipientDb = DatabaseComponent.get(context).recipientDatabase() val threadDB = DatabaseComponent.get(context).threadDatabase() if (userPublicKey == senderPublicKey) { val requestRecipient = Recipient.from(context, fromSerialized(recipientPublicKey), false) recipientDb.setApproved(requestRecipient, true) val threadId = threadDB.getOrCreateThreadIdFor(requestRecipient) threadDB.setHasSent(threadId, true) } else { val mmsDb = DatabaseComponent.get(context).mmsDatabase() val smsDb = DatabaseComponent.get(context).smsDatabase() val sender = Recipient.from(context, fromSerialized(senderPublicKey), false) val threadId = getOrCreateThreadIdFor(sender.address) val profile = response.profile if (profile != null) { val profileManager = SSKEnvironment.shared.profileManager val name = profile.displayName!! if (name.isNotEmpty()) { profileManager.setName(context, sender, name) } val newProfileKey = profile.profileKey val needsProfilePicture = !AvatarHelper.avatarFileExists(context, sender.address) val profileKeyValid = newProfileKey?.isNotEmpty() == true && (newProfileKey.size == 16 || newProfileKey.size == 32) && profile.profilePictureURL?.isNotEmpty() == true val profileKeyChanged = (sender.profileKey == null || !MessageDigest.isEqual(sender.profileKey, newProfileKey)) if ((profileKeyValid && profileKeyChanged) || (profileKeyValid && needsProfilePicture)) { profileManager.setProfilePicture(context, sender, profile.profilePictureURL!!, newProfileKey!!) profileManager.setUnidentifiedAccessMode(context, sender, Recipient.UnidentifiedAccessMode.UNKNOWN) } } threadDB.setHasSent(threadId, true) val mappingDb = DatabaseComponent.get(context).blindedIdMappingDatabase() val mappings = mutableMapOf() threadDB.readerFor(threadDB.conversationList).use { reader -> while (reader.next != null) { val recipient = reader.current.recipient val address = recipient.address.serialize() val blindedId = when { recipient.isGroupRecipient -> null recipient.isOpenGroupInboxRecipient -> { GroupUtil.getDecodedOpenGroupInboxSessionId(address) } else -> { if (SessionId(address).prefix == IdPrefix.BLINDED) { address } else null } } ?: continue mappingDb.getBlindedIdMapping(blindedId).firstOrNull()?.let { mappings[address] = it } } } for (mapping in mappings) { if (!SodiumUtilities.sessionId(senderPublicKey, mapping.value.blindedId, mapping.value.serverId)) { continue } mappingDb.addBlindedIdMapping(mapping.value.copy(sessionId = senderPublicKey)) val blindedThreadId = threadDB.getOrCreateThreadIdFor(Recipient.from(context, fromSerialized(mapping.key), false)) mmsDb.updateThreadId(blindedThreadId, threadId) smsDb.updateThreadId(blindedThreadId, threadId) threadDB.deleteConversation(blindedThreadId) } recipientDb.setApproved(sender, true) recipientDb.setApprovedMe(sender, true) val message = IncomingMediaMessage( sender.address, response.sentTimestamp!!, -1, 0, false, false, true, false, Optional.absent(), Optional.absent(), Optional.absent(), Optional.absent(), Optional.absent(), Optional.absent(), Optional.absent() ) mmsDb.insertSecureDecryptedMessageInbox(message, threadId, runThreadUpdate = true) } } override fun getRecipientApproved(address: Address): Boolean { return address.isClosedGroup || DatabaseComponent.get(context).recipientDatabase().getApproved(address) } override fun setRecipientApproved(recipient: Recipient, approved: Boolean) { DatabaseComponent.get(context).recipientDatabase().setApproved(recipient, approved) if (recipient.isLocalNumber || !recipient.isContactRecipient) return configFactory.contacts?.upsertContact(recipient.address.serialize()) { this.approved = approved } } override fun setRecipientApprovedMe(recipient: Recipient, approvedMe: Boolean) { DatabaseComponent.get(context).recipientDatabase().setApprovedMe(recipient, approvedMe) if (recipient.isLocalNumber || !recipient.isContactRecipient) return configFactory.contacts?.upsertContact(recipient.address.serialize()) { this.approvedMe = approvedMe } } override fun insertCallMessage(senderPublicKey: String, callMessageType: CallMessageType, sentTimestamp: Long) { val database = DatabaseComponent.get(context).smsDatabase() val address = fromSerialized(senderPublicKey) val callMessage = IncomingTextMessage.fromCallInfo(callMessageType, address, Optional.absent(), sentTimestamp) database.insertCallMessage(callMessage) } override fun conversationHasOutgoing(userPublicKey: String): Boolean { val database = DatabaseComponent.get(context).threadDatabase() val threadId = database.getThreadIdIfExistsFor(userPublicKey) if (threadId == -1L) return false return database.getLastSeenAndHasSent(threadId).second() ?: false } override fun getLastInboxMessageId(server: String): Long? { return DatabaseComponent.get(context).lokiAPIDatabase().getLastInboxMessageId(server) } override fun setLastInboxMessageId(server: String, messageId: Long) { DatabaseComponent.get(context).lokiAPIDatabase().setLastInboxMessageId(server, messageId) } override fun removeLastInboxMessageId(server: String) { DatabaseComponent.get(context).lokiAPIDatabase().removeLastInboxMessageId(server) } override fun getLastOutboxMessageId(server: String): Long? { return DatabaseComponent.get(context).lokiAPIDatabase().getLastOutboxMessageId(server) } override fun setLastOutboxMessageId(server: String, messageId: Long) { DatabaseComponent.get(context).lokiAPIDatabase().setLastOutboxMessageId(server, messageId) } override fun removeLastOutboxMessageId(server: String) { DatabaseComponent.get(context).lokiAPIDatabase().removeLastOutboxMessageId(server) } override fun getOrCreateBlindedIdMapping( blindedId: String, server: String, serverPublicKey: String, fromOutbox: Boolean ): BlindedIdMapping { val db = DatabaseComponent.get(context).blindedIdMappingDatabase() val mapping = db.getBlindedIdMapping(blindedId).firstOrNull() ?: BlindedIdMapping(blindedId, null, server, serverPublicKey) if (mapping.sessionId != null) { return mapping } getAllContacts().forEach { contact -> val sessionId = SessionId(contact.sessionID) if (sessionId.prefix == IdPrefix.STANDARD && SodiumUtilities.sessionId(sessionId.hexString(), blindedId, serverPublicKey)) { val contactMapping = mapping.copy(sessionId = sessionId.hexString()) db.addBlindedIdMapping(contactMapping) return contactMapping } } db.getBlindedIdMappingsExceptFor(server).forEach { if (SodiumUtilities.sessionId(it.sessionId!!, blindedId, serverPublicKey)) { val otherMapping = mapping.copy(sessionId = it.sessionId) db.addBlindedIdMapping(otherMapping) return otherMapping } } db.addBlindedIdMapping(mapping) return mapping } override fun addReaction(reaction: Reaction, messageSender: String, notifyUnread: Boolean) { val timestamp = reaction.timestamp val localId = reaction.localId val isMms = reaction.isMms val messageId = if (localId != null && localId > 0 && isMms != null) { MessageId(localId, isMms) } else if (timestamp != null && timestamp > 0) { val messageRecord = DatabaseComponent.get(context).mmsSmsDatabase().getMessageForTimestamp(timestamp) ?: return MessageId(messageRecord.id, messageRecord.isMms) } else return DatabaseComponent.get(context).reactionDatabase().addReaction( messageId, ReactionRecord( messageId = messageId.id, isMms = messageId.mms, author = messageSender, emoji = reaction.emoji!!, serverId = reaction.serverId!!, count = reaction.count!!, sortId = reaction.index!!, dateSent = reaction.dateSent!!, dateReceived = reaction.dateReceived!! ), notifyUnread ) } override fun removeReaction(emoji: String, messageTimestamp: Long, author: String, notifyUnread: Boolean) { val messageRecord = DatabaseComponent.get(context).mmsSmsDatabase().getMessageForTimestamp(messageTimestamp) ?: return val messageId = MessageId(messageRecord.id, messageRecord.isMms) DatabaseComponent.get(context).reactionDatabase().deleteReaction(emoji, messageId, author, notifyUnread) } override fun updateReactionIfNeeded(message: Message, sender: String, openGroupSentTimestamp: Long) { val database = DatabaseComponent.get(context).reactionDatabase() var reaction = database.getReactionFor(message.sentTimestamp!!, sender) ?: return if (openGroupSentTimestamp != -1L) { addReceivedMessageTimestamp(openGroupSentTimestamp) reaction = reaction.copy(dateSent = openGroupSentTimestamp) } message.serverHash?.let { reaction = reaction.copy(serverId = it) } message.openGroupServerMessageID?.let { reaction = reaction.copy(serverId = "$it") } database.updateReaction(reaction) } override fun deleteReactions(messageId: Long, mms: Boolean) { DatabaseComponent.get(context).reactionDatabase().deleteMessageReactions(MessageId(messageId, mms)) } override fun setBlocked(recipients: Iterable, isBlocked: Boolean, fromConfigUpdate: Boolean) { val recipientDb = DatabaseComponent.get(context).recipientDatabase() recipientDb.setBlocked(recipients, isBlocked) recipients.filter { it.isContactRecipient && !it.isLocalNumber }.forEach { recipient -> configFactory.contacts?.upsertContact(recipient.address.serialize()) { this.blocked = isBlocked } } val contactsConfig = configFactory.contacts ?: return if (contactsConfig.needsPush() && !fromConfigUpdate) { ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context) } } override fun blockedContacts(): List { val recipientDb = DatabaseComponent.get(context).recipientDatabase() return recipientDb.blockedContacts } }