mirror of
https://github.com/oxen-io/session-android.git
synced 2025-08-11 20:47:42 +00:00
Merge branch 'dev' into add-unregister
This commit is contained in:
@@ -37,7 +37,7 @@ interface StorageProtocol {
|
||||
fun getUserPublicKey(): String?
|
||||
fun getUserX25519KeyPair(): ECKeyPair
|
||||
fun getUserProfile(): Profile
|
||||
fun setUserProfilePictureURL(newProfilePicture: String)
|
||||
fun setProfileAvatar(recipient: Recipient, profileAvatar: String?)
|
||||
// Signal
|
||||
fun getOrGenerateRegistrationID(): Int
|
||||
|
||||
@@ -106,10 +106,13 @@ interface StorageProtocol {
|
||||
fun getAttachmentsForMessage(messageID: Long): List<DatabaseAttachment>
|
||||
fun getMessageIdInDatabase(timestamp: Long, author: String): Long? // 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 markUnidentified(timestamp: Long, author: String)
|
||||
fun setErrorMessage(timestamp: Long, author: String, error: Exception)
|
||||
fun markAsSyncFailed(timestamp: Long, author: String, error: Exception)
|
||||
fun markAsSentFailed(timestamp: Long, author: String, error: Exception)
|
||||
fun clearErrorMessage(messageID: Long)
|
||||
fun setMessageServerHash(messageID: Long, serverHash: String)
|
||||
|
||||
@@ -156,6 +159,7 @@ interface StorageProtocol {
|
||||
fun trimThread(threadID: Long, threadLimit: Int)
|
||||
fun trimThreadBefore(threadID: Long, timestamp: Long)
|
||||
fun getMessageCount(threadID: Long): Long
|
||||
fun deleteConversation(threadId: Long)
|
||||
|
||||
// Contacts
|
||||
fun getContactWithSessionID(sessionID: String): Contact?
|
||||
@@ -199,6 +203,6 @@ 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 unblock(toUnblock: Iterable<Recipient>)
|
||||
fun blockedContacts(): List<Recipient>
|
||||
}
|
||||
|
@@ -39,13 +39,7 @@ class BackgroundGroupAddJob(val joinUrl: String): Job {
|
||||
delegate?.handleJobFailed(this, dispatcherName, DuplicateGroupException())
|
||||
return
|
||||
}
|
||||
// get image
|
||||
storage.setOpenGroupPublicKey(openGroup.server, openGroup.serverPublicKey)
|
||||
val info = storage.addOpenGroup(openGroup.joinUrl())
|
||||
val imageId = info?.imageId
|
||||
if (imageId != null && storage.getGroupAvatarDownloadJob(openGroup.server, openGroup.room, imageId) == null) {
|
||||
JobQueue.shared.add(GroupAvatarDownloadJob(openGroup.server, openGroup.room, imageId))
|
||||
}
|
||||
storage.addOpenGroup(openGroup.joinUrl())
|
||||
Log.d(KEY, "onOpenGroupAdded(${openGroup.server})")
|
||||
storage.onOpenGroupAdded(openGroup.server)
|
||||
} catch (e: Exception) {
|
||||
|
@@ -125,6 +125,7 @@ class JobQueue : JobDelegate {
|
||||
is NotifyPNServerJob, is AttachmentUploadJob, is MessageSendJob -> {
|
||||
txQueue.send(job)
|
||||
}
|
||||
is RetrieveProfileAvatarJob,
|
||||
is AttachmentDownloadJob -> {
|
||||
mediaQueue.send(job)
|
||||
}
|
||||
@@ -224,6 +225,7 @@ class JobQueue : JobDelegate {
|
||||
GroupAvatarDownloadJob.KEY,
|
||||
BackgroundGroupAddJob.KEY,
|
||||
OpenGroupDeleteJob.KEY,
|
||||
RetrieveProfileAvatarJob.KEY,
|
||||
)
|
||||
allJobTypes.forEach { type ->
|
||||
resumePendingJobs(type)
|
||||
|
@@ -0,0 +1,105 @@
|
||||
package org.session.libsession.messaging.jobs
|
||||
|
||||
import android.text.TextUtils
|
||||
import org.session.libsession.avatars.AvatarHelper
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.utilities.Data
|
||||
import org.session.libsession.utilities.DownloadUtilities.downloadFile
|
||||
import org.session.libsession.utilities.TextSecurePreferences.Companion.setProfileAvatarId
|
||||
import org.session.libsession.utilities.Util.copy
|
||||
import org.session.libsession.utilities.Util.equals
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsession.utilities.TextSecurePreferences.Companion.setProfilePictureURL
|
||||
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
|
||||
|
||||
class RetrieveProfileAvatarJob(private val profileAvatar: String?, private val recipientAddress: Address): Job {
|
||||
override var delegate: JobDelegate? = null
|
||||
override var id: String? = null
|
||||
override var failureCount: Int = 0
|
||||
override val maxFailureCount: Int = 0
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
override fun execute(dispatcherName: String) {
|
||||
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)) {
|
||||
Log.w(TAG, "Recipient profile key is gone!")
|
||||
return
|
||||
}
|
||||
|
||||
if (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))
|
||||
} finally {
|
||||
downloadDestination.delete()
|
||||
}
|
||||
|
||||
if (recipient.isLocalNumber) {
|
||||
setProfileAvatarId(context, SecureRandom().nextInt())
|
||||
setProfilePictureURL(context, profileAvatar)
|
||||
}
|
||||
|
||||
storage.setProfileAvatar(recipient, profileAvatar)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
@@ -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("", "")
|
||||
}
|
||||
|
||||
|
@@ -11,20 +11,22 @@ 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.
|
||||
*/
|
||||
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
|
||||
) : Message() {
|
||||
|
||||
override val isSelfSendValid: Boolean = true
|
||||
|
||||
|
@@ -109,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(
|
||||
|
@@ -61,11 +61,11 @@ object MessageSender {
|
||||
}
|
||||
|
||||
// Convenience
|
||||
fun send(message: Message, destination: Destination): Promise<Unit, Exception> {
|
||||
fun send(message: Message, destination: Destination, isSyncMessage: Boolean = false): Promise<Unit, Exception> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ object MessageSender {
|
||||
val isSelfSend = (message.recipient == userPublicKey)
|
||||
// Set the failure handler (need it here already for precondition failure handling)
|
||||
fun handleFailure(error: Exception) {
|
||||
handleFailedMessageSend(message, error)
|
||||
handleFailedMessageSend(message, error, isSyncMessage)
|
||||
if (destination is Destination.Contact && message is VisibleMessage && !isSelfSend) {
|
||||
SnodeModule.shared.broadcaster.broadcast("messageFailed", message.sentTimestamp!!)
|
||||
}
|
||||
@@ -374,16 +374,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(message.sentTimestamp!!, 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
|
||||
|
@@ -52,19 +52,10 @@ fun MessageSender.create(name: String, members: Collection<String>): Promise<Str
|
||||
storage.createGroup(groupID, name, LinkedList(members.map { Address.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 = SnodeAPI.nowWithOffset
|
||||
for (member in members) {
|
||||
val closedGroupControlMessage = ClosedGroupControlMessage(closedGroupUpdateKind)
|
||||
closedGroupControlMessage.sentTimestamp = sentTime
|
||||
try {
|
||||
sendNonDurably(closedGroupControlMessage, Address.fromSerialized(member)).get()
|
||||
} catch (e: Exception) {
|
||||
deferred.reject(e)
|
||||
return@queue
|
||||
}
|
||||
}
|
||||
|
||||
// Add the group to the user's set of public keys to poll for
|
||||
storage.addClosedGroupPublicKey(groupPublicKey)
|
||||
@@ -73,6 +64,24 @@ fun MessageSender.create(name: String, members: Collection<String>): Promise<Str
|
||||
// Notify the user
|
||||
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||
storage.insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.CREATION, name, members, admins, threadID, sentTime)
|
||||
|
||||
for (member in members) {
|
||||
val closedGroupControlMessage = ClosedGroupControlMessage(closedGroupUpdateKind)
|
||||
closedGroupControlMessage.sentTimestamp = sentTime
|
||||
try {
|
||||
sendNonDurably(closedGroupControlMessage, Address.fromSerialized(member)).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
|
||||
}
|
||||
}
|
||||
|
||||
// Notify the PN server
|
||||
PushNotificationAPI.subscribeGroup(groupPublicKey, userPublicKey)
|
||||
// Start polling
|
||||
|
@@ -5,6 +5,7 @@ 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.jobs.RetrieveProfileAvatarJob
|
||||
import org.session.libsession.messaging.messages.Message
|
||||
import org.session.libsession.messaging.messages.control.CallMessage
|
||||
import org.session.libsession.messaging.messages.control.ClosedGroupControlMessage
|
||||
@@ -183,7 +184,7 @@ private fun handleConfigurationMessage(message: ConfigurationMessage) {
|
||||
ProfileKeyUtil.setEncodedProfileKey(context, profileKey)
|
||||
profileManager.setProfileKey(context, recipient, message.profileKey)
|
||||
if (!message.profilePicture.isNullOrEmpty() && TextSecurePreferences.getProfilePictureURL(context) != message.profilePicture) {
|
||||
storage.setUserProfilePictureURL(message.profilePicture!!)
|
||||
JobQueue.shared.add(RetrieveProfileAvatarJob(message.profilePicture!!, recipient.address))
|
||||
}
|
||||
}
|
||||
storage.addContacts(message.contacts)
|
||||
|
@@ -79,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
|
||||
|
@@ -30,6 +30,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 +40,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,6 +144,7 @@ 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 }
|
||||
|
||||
@@ -86,22 +174,30 @@ class OpenGroupPoller(private val server: String, private val executorService: S
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,82 +206,6 @@ class OpenGroupPoller(private val server: String, private val executorService: S
|
||||
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 dbGroupId = GroupUtil.getEncodedOpenGroupID(groupId.toByteArray())
|
||||
|
||||
val existingOpenGroup = storage.getOpenGroup(roomToken, server)
|
||||
val publicKey = existingOpenGroup?.publicKey ?: return
|
||||
val openGroup = OpenGroup(
|
||||
server = server,
|
||||
room = pollInfo.token,
|
||||
name = if (pollInfo.details != null) { pollInfo.details.name } else { existingOpenGroup.name },
|
||||
publicKey = publicKey,
|
||||
imageId = if (pollInfo.details != null) { pollInfo.details.imageId } else { existingOpenGroup.imageId },
|
||||
canWrite = pollInfo.write,
|
||||
infoUpdates = if (pollInfo.details != null) { pollInfo.details.infoUpdates } else { existingOpenGroup.infoUpdates }
|
||||
)
|
||||
// - 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, existingOpenGroup.imageId))
|
||||
}
|
||||
else if (
|
||||
pollInfo.details != null &&
|
||||
pollInfo.details.imageId == null &&
|
||||
existingOpenGroup.imageId != null
|
||||
) {
|
||||
storage.removeProfilePicture(dbGroupId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleMessages(
|
||||
server: String,
|
||||
roomToken: String,
|
||||
|
@@ -7,16 +7,17 @@ import org.session.libsession.messaging.calls.CallMessageType
|
||||
import org.session.libsession.messaging.contacts.Contact
|
||||
import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage
|
||||
import org.session.libsession.utilities.ExpirationUtil
|
||||
import org.session.libsession.utilities.truncateIdForDisplay
|
||||
|
||||
object UpdateMessageBuilder {
|
||||
|
||||
fun buildGroupUpdateMessage(context: Context, updateMessageData: UpdateMessageData, sender: String? = null, isOutgoing: Boolean = false): String {
|
||||
fun buildGroupUpdateMessage(context: Context, updateMessageData: UpdateMessageData, senderId: String? = null, isOutgoing: Boolean = false): String {
|
||||
var message = ""
|
||||
val updateData = updateMessageData.kind ?: return message
|
||||
if (!isOutgoing && sender == null) return message
|
||||
if (!isOutgoing && senderId == null) return message
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val senderName: String = if (!isOutgoing) {
|
||||
storage.getContactWithSessionID(sender!!)?.displayName(Contact.ContactContext.REGULAR) ?: sender
|
||||
storage.getContactWithSessionID(senderId!!)?.displayName(Contact.ContactContext.REGULAR) ?: truncateIdForDisplay(senderId)
|
||||
} else { context.getString(R.string.MessageRecord_you) }
|
||||
|
||||
when (updateData) {
|
||||
@@ -77,11 +78,11 @@ object UpdateMessageBuilder {
|
||||
return message
|
||||
}
|
||||
|
||||
fun buildExpirationTimerMessage(context: Context, duration: Long, sender: String? = null, isOutgoing: Boolean = false): String {
|
||||
if (!isOutgoing && sender == null) return ""
|
||||
fun buildExpirationTimerMessage(context: Context, duration: Long, senderId: String? = null, isOutgoing: Boolean = false): String {
|
||||
if (!isOutgoing && senderId == null) return ""
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val senderName: String? = if (!isOutgoing) {
|
||||
storage.getContactWithSessionID(sender!!)?.displayName(Contact.ContactContext.REGULAR) ?: sender
|
||||
storage.getContactWithSessionID(senderId!!)?.displayName(Contact.ContactContext.REGULAR) ?: truncateIdForDisplay(senderId)
|
||||
} else { context.getString(R.string.MessageRecord_you) }
|
||||
return if (duration <= 0) {
|
||||
if (isOutgoing) context.getString(R.string.MessageRecord_you_disabled_disappearing_messages)
|
||||
@@ -93,9 +94,9 @@ object UpdateMessageBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
fun buildDataExtractionMessage(context: Context, kind: DataExtractionNotificationInfoMessage.Kind, sender: String? = null): String {
|
||||
fun buildDataExtractionMessage(context: Context, kind: DataExtractionNotificationInfoMessage.Kind, senderId: String? = null): String {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val senderName = storage.getContactWithSessionID(sender!!)?.displayName(Contact.ContactContext.REGULAR) ?: sender!!
|
||||
val senderName = storage.getContactWithSessionID(senderId!!)?.displayName(Contact.ContactContext.REGULAR) ?: truncateIdForDisplay(senderId)
|
||||
return when (kind) {
|
||||
DataExtractionNotificationInfoMessage.Kind.SCREENSHOT ->
|
||||
context.getString(R.string.MessageRecord_s_took_a_screenshot, senderName)
|
||||
|
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -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
|
@@ -13,4 +13,4 @@ fun Context.getColorFromAttr(
|
||||
): Int {
|
||||
theme.resolveAttribute(attrColor, typedValue, resolveRefs)
|
||||
return typedValue.data
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user