mirror of
https://github.com/oxen-io/session-android.git
synced 2025-08-11 21:27:52 +00:00
Merge branch 'dev' of https://github.com/loki-project/session-android into polling-limit-after-inactivity
This commit is contained in:
@@ -1,9 +1,8 @@
|
||||
package org.session.libsession.database
|
||||
|
||||
import org.session.libsession.messaging.open_groups.OpenGroup
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.*
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.messaging.utilities.DotNetAPI
|
||||
import org.session.libsession.utilities.UploadResult
|
||||
import org.session.libsignal.messages.SignalServiceAttachmentPointer
|
||||
import org.session.libsignal.messages.SignalServiceAttachmentStream
|
||||
import java.io.InputStream
|
||||
@@ -13,31 +12,20 @@ interface MessageDataProvider {
|
||||
fun getMessageID(serverID: Long): Long?
|
||||
fun getMessageID(serverId: Long, threadId: Long): Pair<Long, Boolean>?
|
||||
fun deleteMessage(messageID: Long, isSms: Boolean)
|
||||
|
||||
fun getDatabaseAttachment(attachmentId: Long): DatabaseAttachment?
|
||||
|
||||
fun getAttachmentStream(attachmentId: Long): SessionServiceAttachmentStream?
|
||||
fun getAttachmentPointer(attachmentId: Long): SessionServiceAttachmentPointer?
|
||||
|
||||
fun getSignalAttachmentStream(attachmentId: Long): SignalServiceAttachmentStream?
|
||||
fun getScaledSignalAttachmentStream(attachmentId: Long): SignalServiceAttachmentStream?
|
||||
fun getSignalAttachmentPointer(attachmentId: Long): SignalServiceAttachmentPointer?
|
||||
|
||||
fun setAttachmentState(attachmentState: AttachmentState, attachmentId: Long, messageID: Long)
|
||||
|
||||
fun insertAttachment(messageId: Long, attachmentId: AttachmentId, stream : InputStream)
|
||||
|
||||
fun isOutgoingMessage(timestamp: Long): Boolean
|
||||
|
||||
fun handleSuccessfulAttachmentUpload(attachmentId: Long, attachmentStream: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: DotNetAPI.UploadResult)
|
||||
fun handleSuccessfulAttachmentUpload(attachmentId: Long, attachmentStream: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: UploadResult)
|
||||
fun handleFailedAttachmentUpload(attachmentId: Long)
|
||||
|
||||
fun getMessageForQuote(timestamp: Long, author: Address): Pair<Long, Boolean>?
|
||||
fun getAttachmentsAndLinkPreviewFor(mmsId: Long): List<Attachment>
|
||||
fun getMessageBodyFor(timestamp: Long, author: String): String
|
||||
|
||||
fun getAttachmentIDsFor(messageID: Long): List<Long>
|
||||
fun getLinkPreviewAttachmentIDFor(messageID: Long): Long?
|
||||
|
||||
fun getOpenGroup(threadID: Long): OpenGroup?
|
||||
}
|
@@ -1,6 +1,5 @@
|
||||
package org.session.libsession.database
|
||||
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import org.session.libsession.messaging.jobs.AttachmentUploadJob
|
||||
@@ -9,7 +8,6 @@ import org.session.libsession.messaging.jobs.MessageSendJob
|
||||
import org.session.libsession.messaging.messages.control.ConfigurationMessage
|
||||
import org.session.libsession.messaging.messages.visible.Attachment
|
||||
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
||||
import org.session.libsession.messaging.open_groups.OpenGroup
|
||||
import org.session.libsession.messaging.open_groups.OpenGroupV2
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId
|
||||
import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage
|
||||
@@ -18,6 +16,7 @@ import org.session.libsession.messaging.sending_receiving.link_preview.LinkPrevi
|
||||
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.GroupRecord
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.session.libsession.utilities.recipients.Recipient.RecipientSettings
|
||||
import org.session.libsignal.crypto.ecc.ECKeyPair
|
||||
import org.session.libsignal.messages.SignalServiceAttachmentPointer
|
||||
@@ -32,14 +31,12 @@ interface StorageProtocol {
|
||||
fun getUserDisplayName(): String?
|
||||
fun getUserProfileKey(): ByteArray?
|
||||
fun getUserProfilePictureURL(): String?
|
||||
fun setUserProfilePictureUrl(newProfilePicture: String)
|
||||
|
||||
fun setUserProfilePictureURL(newValue: String)
|
||||
fun getProfileKeyForRecipient(recipientPublicKey: String): ByteArray?
|
||||
fun getDisplayNameForRecipient(recipientPublicKey: String): String?
|
||||
fun setProfileKeyForRecipient(recipientPublicKey: String, profileKey: ByteArray)
|
||||
|
||||
// Signal Protocol
|
||||
|
||||
// Signal
|
||||
fun getOrGenerateRegistrationID(): Int
|
||||
|
||||
// Jobs
|
||||
@@ -59,48 +56,40 @@ interface StorageProtocol {
|
||||
|
||||
// Open Groups
|
||||
fun getAllV2OpenGroups(): Map<Long, OpenGroupV2>
|
||||
fun getV2OpenGroup(threadId: String): OpenGroupV2?
|
||||
|
||||
// Open Groups
|
||||
fun getThreadID(openGroupID: String): String?
|
||||
fun addOpenGroup(serverUrl: String, channel: Long)
|
||||
fun getV2OpenGroup(threadId: Long): OpenGroupV2?
|
||||
fun addOpenGroup(urlAsString: String)
|
||||
fun setOpenGroupServerMessageID(messageID: Long, serverID: Long, threadID: Long, isSms: Boolean)
|
||||
fun getQuoteServerID(quoteID: Long, publicKey: String): Long?
|
||||
|
||||
// Open Group Public Keys
|
||||
fun getOpenGroupPublicKey(server: String): String?
|
||||
fun setOpenGroupPublicKey(server: String, newValue: String)
|
||||
|
||||
// Open Group User Info
|
||||
fun setOpenGroupDisplayName(publicKey: String, room: String, server: String, displayName: String)
|
||||
fun getOpenGroupDisplayName(publicKey: String, room: String, server: String): String?
|
||||
|
||||
// Open Group Metadata
|
||||
|
||||
fun updateTitle(groupID: String, newValue: String)
|
||||
fun updateProfilePicture(groupID: String, newValue: ByteArray)
|
||||
fun setUserCount(room: String, server: String, newValue: Int)
|
||||
|
||||
// Last Message Server ID
|
||||
fun getLastMessageServerId(room: String, server: String): Long?
|
||||
fun setLastMessageServerId(room: String, server: String, newValue: Long)
|
||||
fun removeLastMessageServerId(room: String, server: String)
|
||||
fun getLastMessageServerID(room: String, server: String): Long?
|
||||
fun setLastMessageServerID(room: String, server: String, newValue: Long)
|
||||
fun removeLastMessageServerID(room: String, server: String)
|
||||
|
||||
// Last Deletion Server ID
|
||||
fun getLastDeletionServerId(room: String, server: String): Long?
|
||||
fun setLastDeletionServerId(room: String, server: String, newValue: Long)
|
||||
fun removeLastDeletionServerId(room: String, server: String)
|
||||
fun getLastDeletionServerID(room: String, server: String): Long?
|
||||
fun setLastDeletionServerID(room: String, server: String, newValue: Long)
|
||||
fun removeLastDeletionServerID(room: String, server: String)
|
||||
|
||||
// Message Handling
|
||||
fun isDuplicateMessage(timestamp: Long): Boolean
|
||||
fun getReceivedMessageTimestamps(): Set<Long>
|
||||
fun addReceivedMessageTimestamp(timestamp: Long)
|
||||
fun removeReceivedMessageTimestamps(timestamps: Set<Long>)
|
||||
// Returns the IDs of the saved attachments.
|
||||
fun persistAttachments(messageId: Long, attachments: List<Attachment>): List<Long>
|
||||
fun getAttachmentsForMessage(messageId: Long): List<DatabaseAttachment>
|
||||
|
||||
fun getMessageIdInDatabase(timestamp: Long, author: String): Long?
|
||||
/**
|
||||
* Returns the IDs of the saved attachments.
|
||||
*/
|
||||
fun persistAttachments(messageID: Long, attachments: List<Attachment>): List<Long>
|
||||
fun getAttachmentsForMessage(messageID: Long): List<DatabaseAttachment>
|
||||
fun getMessageIdInDatabase(timestamp: Long, author: String): Long? // TODO: This is a weird name
|
||||
fun markAsSent(timestamp: Long, author: String)
|
||||
fun markUnidentified(timestamp: Long, author: String)
|
||||
fun setErrorMessage(timestamp: Long, author: String, error: Exception)
|
||||
@@ -110,11 +99,10 @@ interface StorageProtocol {
|
||||
fun createGroup(groupID: String, title: String?, members: List<Address>, avatar: SignalServiceAttachmentPointer?, relay: String?, admins: List<Address>, formationTimestamp: Long)
|
||||
fun isGroupActive(groupPublicKey: String): Boolean
|
||||
fun setActive(groupID: String, value: Boolean)
|
||||
fun getZombieMember(groupID: String): Set<String>
|
||||
fun getZombieMembers(groupID: String): Set<String>
|
||||
fun removeMember(groupID: String, member: Address)
|
||||
fun updateMembers(groupID: String, members: List<Address>)
|
||||
fun updateZombieMembers(groupID: String, members: List<Address>)
|
||||
// Closed Group
|
||||
fun setZombieMembers(groupID: String, members: List<Address>)
|
||||
fun getAllClosedGroupPublicKeys(): Set<String>
|
||||
fun getAllActiveClosedGroupPublicKeys(): Set<String>
|
||||
fun addClosedGroupPublicKey(groupPublicKey: String)
|
||||
@@ -122,9 +110,9 @@ interface StorageProtocol {
|
||||
fun addClosedGroupEncryptionKeyPair(encryptionKeyPair: ECKeyPair, groupPublicKey: String)
|
||||
fun removeAllClosedGroupEncryptionKeyPairs(groupPublicKey: String)
|
||||
fun insertIncomingInfoMessage(context: Context, senderPublicKey: String, groupID: String, type: SignalServiceGroup.Type,
|
||||
name: String, members: Collection<String>, admins: Collection<String>, sentTimestamp: Long)
|
||||
name: String, members: Collection<String>, admins: Collection<String>, sentTimestamp: Long)
|
||||
fun insertOutgoingInfoMessage(context: Context, groupID: String, type: SignalServiceGroup.Type, name: String,
|
||||
members: Collection<String>, admins: Collection<String>, threadID: Long, sentTimestamp: Long)
|
||||
members: Collection<String>, admins: Collection<String>, threadID: Long, sentTimestamp: Long)
|
||||
fun isClosedGroup(publicKey: String): Boolean
|
||||
fun getClosedGroupEncryptionKeyPairs(groupPublicKey: String): MutableList<ECKeyPair>
|
||||
fun getLatestClosedGroupEncryptionKeyPair(groupPublicKey: String): ECKeyPair?
|
||||
@@ -138,58 +126,27 @@ interface StorageProtocol {
|
||||
// Thread
|
||||
fun getOrCreateThreadIdFor(address: Address): Long
|
||||
fun getOrCreateThreadIdFor(publicKey: String, groupPublicKey: String?, openGroupID: String?): Long
|
||||
fun getThreadIdFor(address: Address): Long?
|
||||
fun getThreadId(publicKeyOrOpenGroupID: String): Long?
|
||||
fun getThreadId(address: Address): Long?
|
||||
fun getThreadId(recipient: Recipient): Long?
|
||||
fun getThreadIdForMms(mmsId: Long): Long
|
||||
fun getLastUpdated(threadID: Long): Long
|
||||
|
||||
// Session Request
|
||||
fun getSessionRequestSentTimestamp(publicKey: String): Long?
|
||||
fun setSessionRequestSentTimestamp(publicKey: String, newValue: Long)
|
||||
fun getSessionRequestProcessedTimestamp(publicKey: String): Long?
|
||||
fun setSessionRequestProcessedTimestamp(publicKey: String, newValue: Long)
|
||||
|
||||
// Loki User
|
||||
// Contacts
|
||||
fun getDisplayName(publicKey: String): String?
|
||||
fun setDisplayName(publicKey: String, newName: String)
|
||||
fun getServerDisplayName(serverID: String, publicKey: String): String?
|
||||
fun getProfilePictureURL(publicKey: String): String?
|
||||
|
||||
// Recipient
|
||||
fun getRecipientSettings(address: Address): RecipientSettings?
|
||||
fun addContacts(contacts: List<ConfigurationMessage.Contact>)
|
||||
|
||||
// PartAuthority
|
||||
// Attachments
|
||||
fun getAttachmentDataUri(attachmentId: AttachmentId): Uri
|
||||
fun getAttachmentThumbnailUri(attachmentId: AttachmentId): Uri
|
||||
|
||||
// Message Handling
|
||||
/// Returns the ID of the `TSIncomingMessage` that was constructed.
|
||||
/**
|
||||
* Returns the ID of the `TSIncomingMessage` that was constructed.
|
||||
*/
|
||||
fun persist(message: VisibleMessage, quotes: QuoteModel?, linkPreview: List<LinkPreview?>, groupPublicKey: String?, openGroupID: String?, attachments: List<Attachment>): Long?
|
||||
|
||||
// Data Extraction Notification
|
||||
fun insertDataExtractionNotificationMessage(senderPublicKey: String, message: DataExtractionNotificationInfoMessage, sentTimestamp: Long)
|
||||
|
||||
// DEPRECATED
|
||||
fun getAuthToken(server: String): String?
|
||||
fun setAuthToken(server: String, newValue: String?)
|
||||
fun removeAuthToken(server: String)
|
||||
|
||||
fun getLastMessageServerID(group: Long, server: String): Long?
|
||||
fun setLastMessageServerID(group: Long, server: String, newValue: Long)
|
||||
fun removeLastMessageServerID(group: Long, server: String)
|
||||
|
||||
fun getLastDeletionServerID(group: Long, server: String): Long?
|
||||
fun setLastDeletionServerID(group: Long, server: String, newValue: Long)
|
||||
fun removeLastDeletionServerID(group: Long, server: String)
|
||||
|
||||
fun getOpenGroup(threadID: String): OpenGroup?
|
||||
fun getAllOpenGroups(): Map<Long, OpenGroup>
|
||||
|
||||
fun setUserCount(group: Long, server: String, newValue: Int)
|
||||
fun setOpenGroupProfilePictureURL(group: Long, server: String, newValue: String)
|
||||
fun getOpenGroupProfilePictureURL(group: Long, server: String): String?
|
||||
|
||||
fun setOpenGroupDisplayName(publicKey: String, channel: Long, server: String, displayName: String)
|
||||
fun getOpenGroupDisplayName(publicKey: String, channel: Long, server: String): String?
|
||||
|
||||
}
|
||||
|
@@ -1,75 +0,0 @@
|
||||
package org.session.libsession.messaging.file_server
|
||||
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.functional.map
|
||||
import okhttp3.Request
|
||||
import org.session.libsession.messaging.utilities.DotNetAPI
|
||||
import org.session.libsession.snode.OnionRequestAPI
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.session.libsignal.utilities.Base64
|
||||
import org.session.libsignal.utilities.JsonUtil
|
||||
import org.session.libsignal.database.LokiAPIDatabaseProtocol
|
||||
import org.session.libsignal.utilities.*
|
||||
import java.net.URL
|
||||
|
||||
class FileServerAPI(public val server: String, userPublicKey: String, userPrivateKey: ByteArray, private val database: LokiAPIDatabaseProtocol) : DotNetAPI() {
|
||||
|
||||
companion object {
|
||||
internal val fileServerPublicKey = "62509D59BDEEC404DD0D489C1E15BA8F94FD3D619B01C1BF48A9922BFCB7311C"
|
||||
internal val maxRetryCount = 4
|
||||
|
||||
public val maxFileSize = 10_000_000 // 10 MB
|
||||
/**
|
||||
* The file server has a file size limit of `maxFileSize`, which the Service Nodes try to enforce as well. However, the limit applied by the Service Nodes
|
||||
* is on the **HTTP request** and not the actual file size. Because the file server expects the file data to be base 64 encoded, the size of the HTTP
|
||||
* request for a given file will be at least `ceil(n / 3) * 4` bytes, where n is the file size in bytes. This is the minimum size because there might also
|
||||
* be other parameters in the request. On average the multiplier appears to be about 1.5, so when checking whether the file will exceed the file size limit when
|
||||
* uploading a file we just divide the size of the file by this number. The alternative would be to actually check the size of the HTTP request but that's only
|
||||
* possible after proof of work has been calculated and the onion request encryption has happened, which takes several seconds.
|
||||
*/
|
||||
public val fileSizeORMultiplier = 2 // TODO: It should be possible to set this to 1.5?
|
||||
public val fileStorageBucketURL = "https://file-static.lokinet.org"
|
||||
// endregion
|
||||
|
||||
// region Initialization
|
||||
lateinit var shared: FileServerAPI
|
||||
|
||||
/**
|
||||
* Must be called before `LokiAPI` is used.
|
||||
*/
|
||||
fun configure(userPublicKey: String, userPrivateKey: ByteArray, database: LokiAPIDatabaseProtocol) {
|
||||
if (Companion::shared.isInitialized) { return }
|
||||
val server = "https://file.getsession.org"
|
||||
shared = FileServerAPI(server, userPublicKey, userPrivateKey, database)
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
|
||||
// region Open Group Server Public Key
|
||||
fun getPublicKeyForOpenGroupServer(openGroupServer: String): Promise<String, Exception> {
|
||||
val publicKey = database.getOpenGroupPublicKey(openGroupServer)
|
||||
if (publicKey != null && PublicKeyValidation.isValid(publicKey, 64, false)) {
|
||||
return Promise.of(publicKey)
|
||||
} else {
|
||||
val url = "$server/loki/v1/getOpenGroupKey/${URL(openGroupServer).host}"
|
||||
val request = Request.Builder().url(url)
|
||||
request.addHeader("Content-Type", "application/json")
|
||||
request.addHeader("Authorization", "Bearer loki") // Tokenless request; use a dummy token
|
||||
return OnionRequestAPI.sendOnionRequest(request.build(), server, fileServerPublicKey).map { json ->
|
||||
try {
|
||||
val bodyAsString = json["data"] as String
|
||||
val body = JsonUtil.fromJson(bodyAsString)
|
||||
val base64EncodedPublicKey = body.get("data").asText()
|
||||
val prefixedPublicKey = Base64.decode(base64EncodedPublicKey)
|
||||
val hexEncodedPrefixedPublicKey = prefixedPublicKey.toHexString()
|
||||
val result = hexEncodedPrefixedPublicKey.removing05PrefixIfNeeded()
|
||||
database.setOpenGroupPublicKey(openGroupServer, result)
|
||||
result
|
||||
} catch (exception: Exception) {
|
||||
Log.d("Loki", "Couldn't parse open group public key from: $json.")
|
||||
throw exception
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -15,10 +15,18 @@ import org.session.libsignal.utilities.Log
|
||||
|
||||
object FileServerAPIV2 {
|
||||
|
||||
private const val OLD_SERVER_PUBLIC_KEY = "7cb31905b55cd5580c686911debf672577b3fb0bff81df4ce2d5c4cb3a7aaa69"
|
||||
const val OLD_SERVER = "http://88.99.175.227"
|
||||
private const val SERVER_PUBLIC_KEY = "da21e1d886c6fbaea313f75298bd64aab03a97ce985b46bb2dad9f2089c8ee59"
|
||||
const val SERVER = "http://filev2.getsession.org"
|
||||
private const val serverPublicKey = "da21e1d886c6fbaea313f75298bd64aab03a97ce985b46bb2dad9f2089c8ee59"
|
||||
const val server = "http://filev2.getsession.org"
|
||||
const val maxFileSize = 10_000_000 // 10 MB
|
||||
/**
|
||||
* The file server has a file size limit of `maxFileSize`, which the Service Nodes try to enforce as well. However, the limit applied by the Service Nodes
|
||||
* is on the **HTTP request** and not the actual file size. Because the file server expects the file data to be base 64 encoded, the size of the HTTP
|
||||
* request for a given file will be at least `ceil(n / 3) * 4` bytes, where n is the file size in bytes. This is the minimum size because there might also
|
||||
* be other parameters in the request. On average the multiplier appears to be about 1.5, so when checking whether the file will exceed the file size limit when
|
||||
* uploading a file we just divide the size of the file by this number. The alternative would be to actually check the size of the HTTP request but that's only
|
||||
* possible after proof of work has been calculated and the onion request encryption has happened, which takes several seconds.
|
||||
*/
|
||||
const val fileSizeORMultiplier = 2 // TODO: It should be possible to set this to 1.5?
|
||||
|
||||
sealed class Error(message: String) : Exception(message) {
|
||||
object ParsingFailed : Error("Invalid response.")
|
||||
@@ -44,9 +52,7 @@ object FileServerAPIV2 {
|
||||
return RequestBody.create(MediaType.get("application/json"), parametersAsJSON)
|
||||
}
|
||||
|
||||
private fun send(request: Request, useOldServer: Boolean): Promise<Map<*, *>, Exception> {
|
||||
val server = if (useOldServer) OLD_SERVER else SERVER
|
||||
val serverPublicKey = if (useOldServer) OLD_SERVER_PUBLIC_KEY else SERVER_PUBLIC_KEY
|
||||
private fun send(request: Request): Promise<Map<*, *>, Exception> {
|
||||
val url = HttpUrl.parse(server) ?: return Promise.ofFail(OpenGroupAPIV2.Error.InvalidURL)
|
||||
val urlBuilder = HttpUrl.Builder()
|
||||
.scheme(url.scheme())
|
||||
@@ -80,14 +86,14 @@ object FileServerAPIV2 {
|
||||
val base64EncodedFile = Base64.encodeBytes(file)
|
||||
val parameters = mapOf( "file" to base64EncodedFile )
|
||||
val request = Request(verb = HTTP.Verb.POST, endpoint = "files", parameters = parameters)
|
||||
return send(request, false).map { json ->
|
||||
return send(request).map { json ->
|
||||
json["result"] as? Long ?: throw OpenGroupAPIV2.Error.ParsingFailed
|
||||
}
|
||||
}
|
||||
|
||||
fun download(file: Long, useOldServer: Boolean): Promise<ByteArray, Exception> {
|
||||
fun download(file: Long): Promise<ByteArray, Exception> {
|
||||
val request = Request(verb = HTTP.Verb.GET, endpoint = "files/$file")
|
||||
return send(request, useOldServer).map { json ->
|
||||
return send(request).map { json ->
|
||||
val base64EncodedFile = json["result"] as? String ?: throw Error.ParsingFailed
|
||||
Base64.decode(base64EncodedFile) ?: throw Error.ParsingFailed
|
||||
}
|
||||
|
@@ -2,11 +2,9 @@ package org.session.libsession.messaging.jobs
|
||||
|
||||
import okhttp3.HttpUrl
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.file_server.FileServerAPI
|
||||
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentState
|
||||
import org.session.libsession.messaging.utilities.Data
|
||||
import org.session.libsession.messaging.utilities.DotNetAPI
|
||||
import org.session.libsession.utilities.DownloadUtilities
|
||||
import org.session.libsignal.streams.AttachmentCipherInputStream
|
||||
import org.session.libsignal.utilities.Base64
|
||||
@@ -42,11 +40,6 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long)
|
||||
if (exception == Error.NoAttachment) {
|
||||
messageDataProvider.setAttachmentState(AttachmentState.FAILED, attachmentID, databaseMessageID)
|
||||
this.handlePermanentFailure(exception)
|
||||
} else if (exception == DotNetAPI.Error.ParsingFailed) {
|
||||
// No need to retry if the response is invalid. Most likely this means we (incorrectly)
|
||||
// got a "Cannot GET ..." error from the file server.
|
||||
messageDataProvider.setAttachmentState(AttachmentState.FAILED, attachmentID, databaseMessageID)
|
||||
this.handlePermanentFailure(exception)
|
||||
} else {
|
||||
this.handleFailure(exception)
|
||||
}
|
||||
@@ -57,9 +50,9 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long)
|
||||
messageDataProvider.setAttachmentState(AttachmentState.STARTED, attachmentID, this.databaseMessageID)
|
||||
val tempFile = createTempFile()
|
||||
val threadID = storage.getThreadIdForMms(databaseMessageID)
|
||||
val openGroupV2 = storage.getV2OpenGroup(threadID.toString())
|
||||
val openGroupV2 = storage.getV2OpenGroup(threadID)
|
||||
val inputStream = if (openGroupV2 == null) {
|
||||
DownloadUtilities.downloadFile(tempFile, attachment.url, FileServerAPI.maxFileSize, null)
|
||||
DownloadUtilities.downloadFile(tempFile, attachment.url)
|
||||
// Assume we're retrieving an attachment for an open group server if the digest is not set
|
||||
if (attachment.digest?.size ?: 0 == 0 || attachment.key.isNullOrEmpty()) {
|
||||
FileInputStream(tempFile)
|
||||
|
@@ -6,13 +6,12 @@ import com.esotericsoftware.kryo.io.Output
|
||||
import nl.komponents.kovenant.Promise
|
||||
import okio.Buffer
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.file_server.FileServerAPI
|
||||
import org.session.libsession.messaging.file_server.FileServerAPIV2
|
||||
import org.session.libsession.messaging.messages.Message
|
||||
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
|
||||
import org.session.libsession.messaging.sending_receiving.MessageSender
|
||||
import org.session.libsession.messaging.utilities.Data
|
||||
import org.session.libsession.messaging.utilities.DotNetAPI
|
||||
import org.session.libsession.utilities.UploadResult
|
||||
import org.session.libsignal.streams.AttachmentCipherOutputStream
|
||||
import org.session.libsignal.messages.SignalServiceAttachmentStream
|
||||
import org.session.libsignal.streams.PaddingInputStream
|
||||
@@ -53,37 +52,28 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
|
||||
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
|
||||
val attachment = messageDataProvider.getScaledSignalAttachmentStream(attachmentID)
|
||||
?: return handleFailure(Error.NoAttachment)
|
||||
val v2OpenGroup = storage.getV2OpenGroup(threadID)
|
||||
val v1OpenGroup = storage.getOpenGroup(threadID)
|
||||
val v2OpenGroup = storage.getV2OpenGroup(threadID.toLong())
|
||||
if (v2OpenGroup != null) {
|
||||
val keyAndResult = upload(attachment, v2OpenGroup.server, false) {
|
||||
OpenGroupAPIV2.upload(it, v2OpenGroup.room, v2OpenGroup.server)
|
||||
}
|
||||
handleSuccess(attachment, keyAndResult.first, keyAndResult.second)
|
||||
} else if (v1OpenGroup == null) {
|
||||
val keyAndResult = upload(attachment, FileServerAPIV2.SERVER, true) {
|
||||
} else {
|
||||
val keyAndResult = upload(attachment, FileServerAPIV2.server, true) {
|
||||
FileServerAPIV2.upload(it)
|
||||
}
|
||||
handleSuccess(attachment, keyAndResult.first, keyAndResult.second)
|
||||
} else { // V1 open group
|
||||
val server = v1OpenGroup.server
|
||||
val pushData = PushAttachmentData(attachment.contentType, attachment.inputStream,
|
||||
attachment.length, PlaintextOutputStreamFactory(), attachment.listener)
|
||||
val result = FileServerAPI.shared.uploadAttachment(server, pushData)
|
||||
handleSuccess(attachment, ByteArray(0), result)
|
||||
}
|
||||
} catch (e: java.lang.Exception) {
|
||||
if (e == Error.NoAttachment) {
|
||||
this.handlePermanentFailure(e)
|
||||
} else if (e is DotNetAPI.Error && !e.isRetryable) {
|
||||
this.handlePermanentFailure(e)
|
||||
} else {
|
||||
this.handleFailure(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun upload(attachment: SignalServiceAttachmentStream, server: String, encrypt: Boolean, upload: (ByteArray) -> Promise<Long, Exception>): Pair<ByteArray, DotNetAPI.UploadResult> {
|
||||
private fun upload(attachment: SignalServiceAttachmentStream, server: String, encrypt: Boolean, upload: (ByteArray) -> Promise<Long, Exception>): Pair<ByteArray, UploadResult> {
|
||||
// Key
|
||||
val key = if (encrypt) Util.getSecretBytes(64) else ByteArray(0)
|
||||
// Length
|
||||
@@ -112,10 +102,10 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
|
||||
val id = upload(data).get()
|
||||
val digest = drb.transmittedDigest
|
||||
// Return
|
||||
return Pair(key, DotNetAPI.UploadResult(id, "${server}/files/$id", digest))
|
||||
return Pair(key, UploadResult(id, "${server}/files/$id", digest))
|
||||
}
|
||||
|
||||
private fun handleSuccess(attachment: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: DotNetAPI.UploadResult) {
|
||||
private fun handleSuccess(attachment: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: UploadResult) {
|
||||
Log.d(TAG, "Attachment uploaded successfully.")
|
||||
delegate?.handleJobSucceeded(this)
|
||||
MessagingModuleConfiguration.shared.messageDataProvider.handleSuccessfulAttachmentUpload(attachmentID, attachment, attachmentKey, uploadResult)
|
||||
|
@@ -30,14 +30,8 @@ class MentionsManager(private val userPublicKey: String, private val userDatabas
|
||||
// Prepare
|
||||
val cache = userPublicKeyCache[threadID] ?: return listOf()
|
||||
// Gather candidates
|
||||
val publicChat = MessagingModuleConfiguration.shared.messageDataProvider.getOpenGroup(threadID)
|
||||
var candidates: List<Mention> = cache.mapNotNull { publicKey ->
|
||||
val displayName: String?
|
||||
if (publicChat != null) {
|
||||
displayName = userDatabase.getServerDisplayName(publicChat.id, publicKey)
|
||||
} else {
|
||||
displayName = userDatabase.getDisplayName(publicKey)
|
||||
}
|
||||
val displayName = userDatabase.getDisplayName(publicKey)
|
||||
if (displayName == null) { return@mapNotNull null }
|
||||
if (displayName.startsWith("Anonymous")) { return@mapNotNull null }
|
||||
Mention(publicKey, displayName)
|
||||
|
@@ -2,7 +2,6 @@ package org.session.libsession.messaging.messages
|
||||
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.open_groups.OpenGroupV2
|
||||
import org.session.libsession.messaging.open_groups.OpenGroup
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsignal.utilities.toHexString
|
||||
@@ -15,9 +14,6 @@ sealed class Destination {
|
||||
class ClosedGroup(var groupPublicKey: String) : Destination() {
|
||||
internal constructor(): this("")
|
||||
}
|
||||
class OpenGroup(var channel: Long, var server: String) : Destination() {
|
||||
internal constructor(): this(0, "")
|
||||
}
|
||||
class OpenGroupV2(var room: String, var server: String) : Destination() {
|
||||
internal constructor(): this("", "")
|
||||
}
|
||||
@@ -36,10 +32,8 @@ sealed class Destination {
|
||||
}
|
||||
address.isOpenGroup -> {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val threadID = storage.getThreadID(address.contactIdentifier())!!
|
||||
when (val openGroup = storage.getV2OpenGroup(threadID) ?: storage.getOpenGroup(threadID)) {
|
||||
is org.session.libsession.messaging.open_groups.OpenGroup
|
||||
-> Destination.OpenGroup(openGroup.channel, openGroup.server)
|
||||
val threadID = storage.getThreadId(address)!!
|
||||
when (val openGroup = storage.getV2OpenGroup(threadID)) {
|
||||
is org.session.libsession.messaging.open_groups.OpenGroupV2
|
||||
-> Destination.OpenGroupV2(openGroup.room, openGroup.server)
|
||||
else -> throw Exception("Missing open group for thread with ID: $threadID.")
|
||||
|
@@ -114,10 +114,9 @@ class ConfigurationMessage(var closedGroups: List<ClosedGroup>, var openGroups:
|
||||
closedGroups.add(closedGroup)
|
||||
}
|
||||
if (group.isOpenGroup) {
|
||||
val threadID = storage.getThreadID(group.encodedId) ?: continue
|
||||
val openGroup = storage.getOpenGroup(threadID)
|
||||
val threadID = storage.getThreadId(group.encodedId) ?: continue
|
||||
val openGroupV2 = storage.getV2OpenGroup(threadID)
|
||||
val shareUrl = openGroup?.server ?: openGroupV2?.joinURL ?: continue
|
||||
val shareUrl = openGroupV2?.joinURL ?: continue
|
||||
openGroups.add(shareUrl)
|
||||
}
|
||||
}
|
||||
|
@@ -1,37 +0,0 @@
|
||||
package org.session.libsession.messaging.open_groups
|
||||
|
||||
import org.session.libsignal.utilities.JsonUtil
|
||||
|
||||
data class OpenGroup(
|
||||
val channel: Long,
|
||||
private val serverURL: String,
|
||||
val displayName: String,
|
||||
val isDeletable: Boolean
|
||||
) {
|
||||
val server get() = serverURL.toLowerCase()
|
||||
val id get() = getId(channel, server)
|
||||
|
||||
companion object {
|
||||
|
||||
@JvmStatic fun getId(channel: Long, server: String): String {
|
||||
return "$server.$channel"
|
||||
}
|
||||
|
||||
@JvmStatic fun fromJSON(jsonAsString: String): OpenGroup? {
|
||||
try {
|
||||
val json = JsonUtil.fromJson(jsonAsString)
|
||||
val channel = json.get("channel").asLong()
|
||||
val server = json.get("server").asText().toLowerCase()
|
||||
val displayName = json.get("displayName").asText()
|
||||
val isDeletable = json.get("isDeletable").asBoolean()
|
||||
return OpenGroup(channel, server, displayName, isDeletable)
|
||||
} catch (e: Exception) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun toJSON(): Map<String, Any> {
|
||||
return mapOf( "channel" to channel, "server" to server, "displayName" to displayName, "isDeletable" to isDeletable )
|
||||
}
|
||||
}
|
@@ -1,394 +0,0 @@
|
||||
package org.session.libsession.messaging.open_groups
|
||||
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.deferred
|
||||
import nl.komponents.kovenant.functional.map
|
||||
import nl.komponents.kovenant.then
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.file_server.FileServerAPI
|
||||
import org.session.libsession.messaging.utilities.DotNetAPI
|
||||
import org.session.libsession.utilities.DownloadUtilities
|
||||
import org.session.libsignal.utilities.retryIfNeeded
|
||||
import org.session.libsignal.utilities.*
|
||||
import org.session.libsignal.utilities.Base64
|
||||
import org.session.libsignal.utilities.Log
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
object OpenGroupAPI: DotNetAPI() {
|
||||
|
||||
private val moderators: HashMap<String, HashMap<Long, Set<String>>> = hashMapOf() // Server URL to (channel ID to set of moderator IDs)
|
||||
|
||||
// region Settings
|
||||
private val fallbackBatchCount = 64
|
||||
private val maxRetryCount = 8
|
||||
// endregion
|
||||
|
||||
// region Convenience
|
||||
private val channelInfoType = "net.patter-app.settings"
|
||||
private val attachmentType = "net.app.core.oembed"
|
||||
@JvmStatic
|
||||
val openGroupMessageType = "network.loki.messenger.publicChat"
|
||||
@JvmStatic
|
||||
val profilePictureType = "network.loki.messenger.avatar"
|
||||
|
||||
fun getDefaultChats(): List<OpenGroup> {
|
||||
return listOf() // Don't auto-join any open groups right now
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun isUserModerator(hexEncodedPublicKey: String, channel: Long, server: String): Boolean {
|
||||
if (moderators[server] != null && moderators[server]!![channel] != null) {
|
||||
return moderators[server]!![channel]!!.contains(hexEncodedPublicKey)
|
||||
}
|
||||
return false
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Public API
|
||||
fun getMessages(channel: Long, server: String): Promise<List<OpenGroupMessage>, Exception> {
|
||||
Log.d("Loki", "Getting messages for open group with ID: $channel on server: $server.")
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val parameters = mutableMapOf<String, Any>( "include_annotations" to 1 )
|
||||
val lastMessageServerID = storage.getLastMessageServerID(channel, server)
|
||||
if (lastMessageServerID != null) {
|
||||
parameters["since_id"] = lastMessageServerID
|
||||
} else {
|
||||
parameters["count"] = fallbackBatchCount
|
||||
parameters["include_deleted"] = 0
|
||||
}
|
||||
return execute(HTTPVerb.GET, server, "channels/$channel/messages", parameters = parameters).then { json ->
|
||||
try {
|
||||
val data = json["data"] as List<Map<*, *>>
|
||||
val messages = data.mapNotNull { message ->
|
||||
try {
|
||||
val isDeleted = message["is_deleted"] as? Boolean ?: false
|
||||
if (isDeleted) { return@mapNotNull null }
|
||||
// Ignore messages without annotations
|
||||
if (message["annotations"] == null) { return@mapNotNull null }
|
||||
val annotation = (message["annotations"] as List<Map<*, *>>).find {
|
||||
((it["type"] as? String ?: "") == openGroupMessageType) && it["value"] != null
|
||||
} ?: return@mapNotNull null
|
||||
val value = annotation["value"] as Map<*, *>
|
||||
val serverID = message["id"] as? Long ?: (message["id"] as? Int)?.toLong() ?: (message["id"] as String).toLong()
|
||||
val user = message["user"] as Map<*, *>
|
||||
val publicKey = user["username"] as String
|
||||
val displayName = user["name"] as? String ?: "Anonymous"
|
||||
var profilePicture: OpenGroupMessage.ProfilePicture? = null
|
||||
if (user["annotations"] != null) {
|
||||
val profilePictureAnnotation = (user["annotations"] as List<Map< *, *>>).find {
|
||||
((it["type"] as? String ?: "") == profilePictureType) && it["value"] != null
|
||||
}
|
||||
val profilePictureAnnotationValue = profilePictureAnnotation?.get("value") as? Map<*, *>
|
||||
if (profilePictureAnnotationValue != null && profilePictureAnnotationValue["profileKey"] != null && profilePictureAnnotationValue["url"] != null) {
|
||||
try {
|
||||
val profileKey = Base64.decode(profilePictureAnnotationValue["profileKey"] as String)
|
||||
val url = profilePictureAnnotationValue["url"] as String
|
||||
profilePicture = OpenGroupMessage.ProfilePicture(profileKey, url)
|
||||
} catch (e: Exception) {}
|
||||
}
|
||||
}
|
||||
@Suppress("NAME_SHADOWING") val body = message["text"] as String
|
||||
val timestamp = value["timestamp"] as? Long ?: (value["timestamp"] as? Int)?.toLong() ?: (value["timestamp"] as String).toLong()
|
||||
var quote: OpenGroupMessage.Quote? = null
|
||||
if (value["quote"] != null) {
|
||||
val replyTo = message["reply_to"] as? Long ?: (message["reply_to"] as? Int)?.toLong() ?: (message["reply_to"] as String).toLong()
|
||||
val quoteAnnotation = value["quote"] as? Map<*, *>
|
||||
val quoteTimestamp = quoteAnnotation?.get("id") as? Long ?: (quoteAnnotation?.get("id") as? Int)?.toLong() ?: (quoteAnnotation?.get("id") as? String)?.toLong() ?: 0L
|
||||
val author = quoteAnnotation?.get("author") as? String
|
||||
val text = quoteAnnotation?.get("text") as? String
|
||||
quote = if (quoteTimestamp > 0L && author != null && text != null) OpenGroupMessage.Quote(quoteTimestamp, author, text, replyTo) else null
|
||||
}
|
||||
val attachmentsAsJSON = (message["annotations"] as List<Map<*, *>>).filter {
|
||||
((it["type"] as? String ?: "") == attachmentType) && it["value"] != null
|
||||
}
|
||||
val attachments = attachmentsAsJSON.mapNotNull { it["value"] as? Map<*, *> }.mapNotNull { attachmentAsJSON ->
|
||||
try {
|
||||
val kindAsString = attachmentAsJSON["lokiType"] as String
|
||||
val kind = OpenGroupMessage.Attachment.Kind.values().first { it.rawValue == kindAsString }
|
||||
val id = attachmentAsJSON["id"] as? Long ?: (attachmentAsJSON["id"] as? Int)?.toLong() ?: (attachmentAsJSON["id"] as String).toLong()
|
||||
val contentType = attachmentAsJSON["contentType"] as String
|
||||
val size = attachmentAsJSON["size"] as? Int ?: (attachmentAsJSON["size"] as? Long)?.toInt() ?: (attachmentAsJSON["size"] as String).toInt()
|
||||
val fileName = attachmentAsJSON["fileName"] as? String
|
||||
val flags = 0
|
||||
val url = attachmentAsJSON["url"] as String
|
||||
val caption = attachmentAsJSON["caption"] as? String
|
||||
val linkPreviewURL = attachmentAsJSON["linkPreviewUrl"] as? String
|
||||
val linkPreviewTitle = attachmentAsJSON["linkPreviewTitle"] as? String
|
||||
if (kind == OpenGroupMessage.Attachment.Kind.LinkPreview && (linkPreviewURL == null || linkPreviewTitle == null)) {
|
||||
null
|
||||
} else {
|
||||
OpenGroupMessage.Attachment(kind, server, id, contentType, size, fileName, flags, 0, 0, caption, url, linkPreviewURL, linkPreviewTitle)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.d("Loki","Couldn't parse attachment due to error: $e.")
|
||||
null
|
||||
}
|
||||
}
|
||||
// Set the last message server ID here to avoid the situation where a message doesn't have a valid signature and this function is called over and over
|
||||
@Suppress("NAME_SHADOWING") val lastMessageServerID = storage.getLastMessageServerID(channel, server)
|
||||
if (serverID > lastMessageServerID ?: 0) { storage.setLastMessageServerID(channel, server, serverID) }
|
||||
val hexEncodedSignature = value["sig"] as String
|
||||
val signatureVersion = value["sigver"] as? Long ?: (value["sigver"] as? Int)?.toLong() ?: (value["sigver"] as String).toLong()
|
||||
val signature = OpenGroupMessage.Signature(Hex.fromStringCondensed(hexEncodedSignature), signatureVersion)
|
||||
val format = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US)
|
||||
format.timeZone = TimeZone.getTimeZone("GMT")
|
||||
val dateAsString = message["created_at"] as String
|
||||
val serverTimestamp = format.parse(dateAsString).time
|
||||
// Verify the message
|
||||
val groupMessage = OpenGroupMessage(serverID, publicKey, displayName, body, timestamp, openGroupMessageType, quote, attachments.toMutableList(), profilePicture, signature, serverTimestamp)
|
||||
if (groupMessage.hasValidSignature()) groupMessage else null
|
||||
} catch (exception: Exception) {
|
||||
Log.d("Loki", "Couldn't parse message for open group with ID: $channel on server: $server from: ${JsonUtil.toJson(message)}. Exception: ${exception.message}")
|
||||
return@mapNotNull null
|
||||
}
|
||||
}.sortedBy { it.serverTimestamp }
|
||||
messages
|
||||
} catch (exception: Exception) {
|
||||
Log.d("Loki", "Couldn't parse messages for open group with ID: $channel on server: $server.")
|
||||
throw exception
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getDeletedMessageServerIDs(channel: Long, server: String): Promise<List<Long>, Exception> {
|
||||
Log.d("Loki", "Getting deleted messages for open group with ID: $channel on server: $server.")
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val parameters = mutableMapOf<String, Any>()
|
||||
val lastDeletionServerID = storage.getLastDeletionServerID(channel, server)
|
||||
if (lastDeletionServerID != null) {
|
||||
parameters["since_id"] = lastDeletionServerID
|
||||
} else {
|
||||
parameters["count"] = fallbackBatchCount
|
||||
}
|
||||
return execute(HTTPVerb.GET, server, "loki/v1/channel/$channel/deletes", parameters = parameters).then { json ->
|
||||
try {
|
||||
val deletedMessageServerIDs = (json["data"] as List<Map<*, *>>).mapNotNull { deletion ->
|
||||
try {
|
||||
val serverID = deletion["id"] as? Long ?: (deletion["id"] as? Int)?.toLong() ?: (deletion["id"] as String).toLong()
|
||||
val messageServerID = deletion["message_id"] as? Long ?: (deletion["message_id"] as? Int)?.toLong() ?: (deletion["message_id"] as String).toLong()
|
||||
@Suppress("NAME_SHADOWING") val lastDeletionServerID = storage.getLastDeletionServerID(channel, server)
|
||||
if (serverID > (lastDeletionServerID ?: 0)) { storage.setLastDeletionServerID(channel, server, serverID) }
|
||||
messageServerID
|
||||
} catch (exception: Exception) {
|
||||
Log.d("Loki", "Couldn't parse deleted message for open group with ID: $channel on server: $server. Exception: ${exception.message}")
|
||||
return@mapNotNull null
|
||||
}
|
||||
}
|
||||
deletedMessageServerIDs
|
||||
} catch (exception: Exception) {
|
||||
Log.d("Loki", "Couldn't parse deleted messages for open group with ID: $channel on server: $server.")
|
||||
throw exception
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun sendMessage(message: OpenGroupMessage, channel: Long, server: String): Promise<OpenGroupMessage, Exception> {
|
||||
val deferred = deferred<OpenGroupMessage, Exception>()
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val userKeyPair = storage.getUserKeyPair() ?: throw Error.Generic
|
||||
val userDisplayName = storage.getUserDisplayName() ?: throw Error.Generic
|
||||
ThreadUtils.queue {
|
||||
val signedMessage = message.sign(userKeyPair.second)
|
||||
if (signedMessage == null) {
|
||||
deferred.reject(Error.SigningFailed)
|
||||
} else {
|
||||
retryIfNeeded(maxRetryCount) {
|
||||
Log.d("Loki", "Sending message to open group with ID: $channel on server: $server.")
|
||||
val parameters = signedMessage.toJSON()
|
||||
execute(HTTPVerb.POST, server, "channels/$channel/messages", parameters = parameters).then { json ->
|
||||
try {
|
||||
val data = json["data"] as Map<*, *>
|
||||
val serverID = (data["id"] as? Long) ?: (data["id"] as? Int)?.toLong() ?: (data["id"] as String).toLong()
|
||||
val text = data["text"] as String
|
||||
val format = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US)
|
||||
format.timeZone = TimeZone.getTimeZone("GMT")
|
||||
val dateAsString = data["created_at"] as String
|
||||
val timestamp = format.parse(dateAsString).time
|
||||
OpenGroupMessage(serverID, userKeyPair.first, userDisplayName, text, timestamp, openGroupMessageType, message.quote, message.attachments, null, signedMessage.signature, timestamp)
|
||||
} catch (exception: Exception) {
|
||||
Log.d("Loki", "Couldn't parse message for open group with ID: $channel on server: $server.")
|
||||
throw exception
|
||||
}
|
||||
}
|
||||
}.success {
|
||||
deferred.resolve(it)
|
||||
}.fail {
|
||||
deferred.reject(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
return deferred.promise
|
||||
}
|
||||
|
||||
fun deleteMessage(messageServerID: Long, channel: Long, server: String, isSentByUser: Boolean): Promise<Long, Exception> {
|
||||
return retryIfNeeded(maxRetryCount) {
|
||||
val isModerationRequest = !isSentByUser
|
||||
Log.d("Loki", "Deleting message with ID: $messageServerID from open group with ID: $channel on server: $server (isModerationRequest = $isModerationRequest).")
|
||||
val endpoint = if (isSentByUser) "channels/$channel/messages/$messageServerID" else "loki/v1/moderation/message/$messageServerID"
|
||||
execute(HTTPVerb.DELETE, server, endpoint, isJSONRequired = false).then {
|
||||
Log.d("Loki", "Deleted message with ID: $messageServerID from open group with ID: $channel on server: $server.")
|
||||
messageServerID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun deleteMessages(messageServerIDs: List<Long>, channel: Long, server: String, isSentByUser: Boolean): Promise<List<Long>, Exception> {
|
||||
return retryIfNeeded(maxRetryCount) {
|
||||
val isModerationRequest = !isSentByUser
|
||||
val parameters = mapOf( "ids" to messageServerIDs.joinToString(",") )
|
||||
Log.d("Loki", "Deleting messages with IDs: ${messageServerIDs.joinToString()} from open group with ID: $channel on server: $server (isModerationRequest = $isModerationRequest).")
|
||||
val endpoint = if (isSentByUser) "loki/v1/messages" else "loki/v1/moderation/messages"
|
||||
execute(HTTPVerb.DELETE, server, endpoint, parameters = parameters, isJSONRequired = false).then { json ->
|
||||
Log.d("Loki", "Deleted messages with IDs: $messageServerIDs from open group with ID: $channel on server: $server.")
|
||||
messageServerIDs
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getModerators(channel: Long, server: String): Promise<Set<String>, Exception> {
|
||||
return execute(HTTPVerb.GET, server, "loki/v1/channel/$channel/get_moderators").then { json ->
|
||||
try {
|
||||
@Suppress("UNCHECKED_CAST") val moderators = json["moderators"] as? List<String>
|
||||
val moderatorsAsSet = moderators.orEmpty().toSet()
|
||||
if (this.moderators[server] != null) {
|
||||
this.moderators[server]!![channel] = moderatorsAsSet
|
||||
} else {
|
||||
this.moderators[server] = hashMapOf( channel to moderatorsAsSet )
|
||||
}
|
||||
moderatorsAsSet
|
||||
} catch (exception: Exception) {
|
||||
Log.d("Loki", "Couldn't parse moderators for open group with ID: $channel on server: $server.")
|
||||
throw exception
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getChannelInfo(channel: Long, server: String): Promise<OpenGroupInfo, Exception> {
|
||||
return retryIfNeeded(maxRetryCount) {
|
||||
val parameters = mapOf( "include_annotations" to 1 )
|
||||
execute(HTTPVerb.GET, server, "/channels/$channel", parameters = parameters).then { json ->
|
||||
try {
|
||||
val data = json["data"] as Map<*, *>
|
||||
val annotations = data["annotations"] as List<Map<*, *>>
|
||||
val annotation = annotations.find { (it["type"] as? String ?: "") == channelInfoType } ?: throw Error.ParsingFailed
|
||||
val info = annotation["value"] as Map<*, *>
|
||||
val displayName = info["name"] as String
|
||||
val countInfo = data["counts"] as Map<*, *>
|
||||
val memberCount = countInfo["subscribers"] as? Int ?: (countInfo["subscribers"] as? Long)?.toInt() ?: (countInfo["subscribers"] as String).toInt()
|
||||
val profilePictureURL = info["avatar"] as String
|
||||
val publicChatInfo = OpenGroupInfo(displayName, profilePictureURL, memberCount)
|
||||
MessagingModuleConfiguration.shared.storage.setUserCount(channel, server, memberCount)
|
||||
publicChatInfo
|
||||
} catch (exception: Exception) {
|
||||
Log.d("Loki", "Couldn't parse info for open group with ID: $channel on server: $server.")
|
||||
throw exception
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun updateProfileIfNeeded(channel: Long, server: String, groupID: String, info: OpenGroupInfo, isForcedUpdate: Boolean) {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
storage.setUserCount(channel, server, info.memberCount)
|
||||
storage.updateTitle(groupID, info.displayName)
|
||||
// Download and update profile picture if needed
|
||||
val oldProfilePictureURL = storage.getOpenGroupProfilePictureURL(channel, server)
|
||||
if (isForcedUpdate || oldProfilePictureURL != info.profilePictureURL) {
|
||||
val profilePictureAsByteArray = downloadOpenGroupProfilePicture(server, info.profilePictureURL) ?: return
|
||||
storage.updateProfilePicture(groupID, profilePictureAsByteArray)
|
||||
storage.setOpenGroupProfilePictureURL(channel, server, info.profilePictureURL)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun downloadOpenGroupProfilePicture(server: String, endpoint: String): ByteArray? {
|
||||
val url = "${server.removeSuffix("/")}/${endpoint.removePrefix("/")}"
|
||||
Log.d("Loki", "Downloading open group profile picture from \"$url\".")
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
try {
|
||||
DownloadUtilities.downloadFile(outputStream, url, FileServerAPI.maxFileSize, null)
|
||||
Log.d("Loki", "Open group profile picture was successfully loaded from \"$url\"")
|
||||
return outputStream.toByteArray()
|
||||
} catch (e: Exception) {
|
||||
Log.d("Loki", "Failed to download open group profile picture from \"$url\" due to error: $e.")
|
||||
return null
|
||||
} finally {
|
||||
outputStream.close()
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun join(channel: Long, server: String): Promise<Unit, Exception> {
|
||||
return retryIfNeeded(maxRetryCount) {
|
||||
execute(HTTPVerb.POST, server, "/channels/$channel/subscribe").then {
|
||||
Log.d("Loki", "Joined channel with ID: $channel on server: $server.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun leave(channel: Long, server: String): Promise<Unit, Exception> {
|
||||
return retryIfNeeded(maxRetryCount) {
|
||||
execute(HTTPVerb.DELETE, server, "/channels/$channel/subscribe").then {
|
||||
Log.d("Loki", "Left channel with ID: $channel on server: $server.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun ban(publicKey: String, server: String): Promise<Unit,Exception> {
|
||||
return retryIfNeeded(maxRetryCount) {
|
||||
execute(HTTPVerb.POST, server, "/loki/v1/moderation/blacklist/@$publicKey").then {
|
||||
Log.d("Loki", "Banned user with ID: $publicKey from $server")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getDisplayNames(publicKeys: Set<String>, server: String): Promise<Map<String, String>, Exception> {
|
||||
return getUserProfiles(publicKeys, server, false).map { json ->
|
||||
val mapping = mutableMapOf<String, String>()
|
||||
for (user in json) {
|
||||
if (user["username"] != null) {
|
||||
val publicKey = user["username"] as String
|
||||
val displayName = user["name"] as? String ?: "Anonymous"
|
||||
mapping[publicKey] = displayName
|
||||
}
|
||||
}
|
||||
mapping
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun setDisplayName(newDisplayName: String?, server: String): Promise<Unit, Exception> {
|
||||
Log.d("Loki", "Updating display name on server: $server.")
|
||||
val parameters = mapOf( "name" to (newDisplayName ?: "") )
|
||||
return execute(HTTPVerb.PATCH, server, "users/me", parameters = parameters).map { Unit }
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun setProfilePicture(server: String, profileKey: ByteArray, url: String?): Promise<Unit, Exception> {
|
||||
return setProfilePicture(server, Base64.encodeBytes(profileKey), url)
|
||||
}
|
||||
|
||||
fun setProfilePicture(server: String, profileKey: String, url: String?): Promise<Unit, Exception> {
|
||||
Log.d("Loki", "Updating profile picture on server: $server.")
|
||||
val value = when (url) {
|
||||
null -> null
|
||||
else -> mapOf( "profileKey" to profileKey, "url" to url )
|
||||
}
|
||||
// TODO: This may actually completely replace the annotations, have to double check it
|
||||
return setSelfAnnotation(server, profilePictureType, value).map { Unit }.fail {
|
||||
Log.d("Loki", "Failed to update profile picture due to error: $it.")
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
}
|
@@ -31,8 +31,8 @@ object OpenGroupAPIV2 {
|
||||
private val curve = Curve25519.getInstance(Curve25519.BEST)
|
||||
val defaultRooms = MutableSharedFlow<List<DefaultGroup>>(replay = 1)
|
||||
|
||||
private const val DEFAULT_SERVER_PUBLIC_KEY = "a03c383cf63c3c4efe67acc52112a6dd734b3a946b9545f488aaa93da7991238"
|
||||
const val DEFAULT_SERVER = "http://116.203.70.33"
|
||||
private const val defaultServerPublicKey = "a03c383cf63c3c4efe67acc52112a6dd734b3a946b9545f488aaa93da7991238"
|
||||
const val defaultServer = "http://116.203.70.33"
|
||||
|
||||
sealed class Error(message: String) : Exception(message) {
|
||||
object Generic : Error("An error occurred.")
|
||||
@@ -45,7 +45,7 @@ object OpenGroupAPIV2 {
|
||||
|
||||
data class DefaultGroup(val id: String, val name: String, val image: ByteArray?) {
|
||||
|
||||
val joinURL: String get() = "$DEFAULT_SERVER/$id?public_key=$DEFAULT_SERVER_PUBLIC_KEY"
|
||||
val joinURL: String get() = "$defaultServer/$id?public_key=$defaultServerPublicKey"
|
||||
}
|
||||
|
||||
data class Info(val id: String, val name: String, val imageID: String?)
|
||||
@@ -60,7 +60,7 @@ object OpenGroupAPIV2 {
|
||||
) {
|
||||
|
||||
companion object {
|
||||
val EMPTY = MessageDeletion()
|
||||
val empty = MessageDeletion()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,9 +125,7 @@ object OpenGroupAPIV2 {
|
||||
if (e is OnionRequestAPI.HTTPRequestFailedAtDestinationException && e.statusCode == 401) {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
if (request.room != null) {
|
||||
storage.removeAuthToken("${request.server}.${request.room}")
|
||||
} else {
|
||||
storage.removeAuthToken(request.server)
|
||||
storage.removeAuthToken(request.room, request.server)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -237,7 +235,7 @@ object OpenGroupAPIV2 {
|
||||
fun getMessages(room: String, server: String): Promise<List<OpenGroupMessageV2>, Exception> {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val queryParameters = mutableMapOf<String, String>()
|
||||
storage.getLastMessageServerId(room, server)?.let { lastId ->
|
||||
storage.getLastMessageServerID(room, server)?.let { lastId ->
|
||||
queryParameters += "from_server_id" to lastId.toString()
|
||||
}
|
||||
val request = Request(verb = GET, room = room, server = server, endpoint = "messages", queryParameters = queryParameters)
|
||||
@@ -250,7 +248,7 @@ object OpenGroupAPIV2 {
|
||||
|
||||
private fun parseMessages(room: String, server: String, rawMessages: List<Map<*, *>>): List<OpenGroupMessageV2> {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val lastMessageServerID = storage.getLastMessageServerId(room, server) ?: 0
|
||||
val lastMessageServerID = storage.getLastMessageServerID(room, server) ?: 0
|
||||
var currentLastMessageServerID = lastMessageServerID
|
||||
val messages = rawMessages.mapNotNull { json ->
|
||||
json as Map<String, Any>
|
||||
@@ -274,7 +272,7 @@ object OpenGroupAPIV2 {
|
||||
null
|
||||
}
|
||||
}
|
||||
storage.setLastMessageServerId(room, server, currentLastMessageServerID)
|
||||
storage.setLastMessageServerID(room, server, currentLastMessageServerID)
|
||||
return messages
|
||||
}
|
||||
// endregion
|
||||
@@ -291,7 +289,7 @@ object OpenGroupAPIV2 {
|
||||
fun getDeletedMessages(room: String, server: String): Promise<List<MessageDeletion>, Exception> {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val queryParameters = mutableMapOf<String, String>()
|
||||
storage.getLastDeletionServerId(room, server)?.let { last ->
|
||||
storage.getLastDeletionServerID(room, server)?.let { last ->
|
||||
queryParameters["from_server_id"] = last.toString()
|
||||
}
|
||||
val request = Request(verb = GET, room = room, server = server, endpoint = "deleted_messages", queryParameters = queryParameters)
|
||||
@@ -299,10 +297,10 @@ object OpenGroupAPIV2 {
|
||||
val type = TypeFactory.defaultInstance().constructCollectionType(List::class.java, MessageDeletion::class.java)
|
||||
val idsAsString = JsonUtil.toJson(json["ids"])
|
||||
val serverIDs = JsonUtil.fromJson<List<MessageDeletion>>(idsAsString, type) ?: throw Error.ParsingFailed
|
||||
val lastMessageServerId = storage.getLastDeletionServerId(room, server) ?: 0
|
||||
val serverID = serverIDs.maxByOrNull {it.id } ?: MessageDeletion.EMPTY
|
||||
val lastMessageServerId = storage.getLastDeletionServerID(room, server) ?: 0
|
||||
val serverID = serverIDs.maxByOrNull {it.id } ?: MessageDeletion.empty
|
||||
if (serverID.id > lastMessageServerId) {
|
||||
storage.setLastDeletionServerId(room, server, serverID.id)
|
||||
storage.setLastDeletionServerID(room, server, serverID.id)
|
||||
}
|
||||
serverIDs
|
||||
}
|
||||
@@ -361,8 +359,8 @@ object OpenGroupAPIV2 {
|
||||
CompactPollRequest(
|
||||
roomID = room,
|
||||
authToken = authToken,
|
||||
fromDeletionServerID = storage.getLastDeletionServerId(room, server),
|
||||
fromMessageServerID = storage.getLastMessageServerId(room, server)
|
||||
fromDeletionServerID = storage.getLastDeletionServerID(room, server),
|
||||
fromMessageServerID = storage.getLastMessageServerID(room, server)
|
||||
)
|
||||
}
|
||||
val request = Request(verb = POST, room = null, server = server, endpoint = "compact_poll", isAuthRequired = false, parameters = mapOf( "requests" to requests ))
|
||||
@@ -386,10 +384,10 @@ object OpenGroupAPIV2 {
|
||||
val type = TypeFactory.defaultInstance().constructCollectionType(List::class.java, MessageDeletion::class.java)
|
||||
val idsAsString = JsonUtil.toJson(json["deletions"])
|
||||
val deletedServerIDs = JsonUtil.fromJson<List<MessageDeletion>>(idsAsString, type) ?: throw Error.ParsingFailed
|
||||
val lastDeletionServerID = storage.getLastDeletionServerId(roomID, server) ?: 0
|
||||
val serverID = deletedServerIDs.maxByOrNull { it.id } ?: MessageDeletion.EMPTY
|
||||
val lastDeletionServerID = storage.getLastDeletionServerID(roomID, server) ?: 0
|
||||
val serverID = deletedServerIDs.maxByOrNull { it.id } ?: MessageDeletion.empty
|
||||
if (serverID.id > lastDeletionServerID) {
|
||||
storage.setLastDeletionServerId(roomID, server, serverID.id)
|
||||
storage.setLastDeletionServerID(roomID, server, serverID.id)
|
||||
}
|
||||
// Messages
|
||||
val rawMessages = json["messages"] as? List<Map<String, Any>> ?: return@mapNotNull null
|
||||
@@ -405,8 +403,8 @@ object OpenGroupAPIV2 {
|
||||
|
||||
fun getDefaultRoomsIfNeeded(): Promise<List<DefaultGroup>, Exception> {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
storage.setOpenGroupPublicKey(DEFAULT_SERVER, DEFAULT_SERVER_PUBLIC_KEY)
|
||||
return getAllRooms(DEFAULT_SERVER).map { groups ->
|
||||
storage.setOpenGroupPublicKey(defaultServer, defaultServerPublicKey)
|
||||
return getAllRooms(defaultServer).map { groups ->
|
||||
val earlyGroups = groups.map { group ->
|
||||
DefaultGroup(group.id, group.name, null)
|
||||
}
|
||||
@@ -417,7 +415,7 @@ object OpenGroupAPIV2 {
|
||||
}
|
||||
}
|
||||
val images = groups.map { group ->
|
||||
group.id to downloadOpenGroupProfilePicture(group.id, DEFAULT_SERVER)
|
||||
group.id to downloadOpenGroupProfilePicture(group.id, defaultServer)
|
||||
}.toMap()
|
||||
groups.map { group ->
|
||||
val image = try {
|
||||
|
@@ -1,247 +0,0 @@
|
||||
package org.session.libsession.messaging.open_groups
|
||||
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
||||
import org.session.libsignal.utilities.removing05PrefixIfNeeded
|
||||
import org.session.libsignal.utilities.Hex
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.whispersystems.curve25519.Curve25519
|
||||
|
||||
data class OpenGroupMessage(
|
||||
val serverID: Long?,
|
||||
val senderPublicKey: String,
|
||||
val displayName: String,
|
||||
val body: String,
|
||||
val timestamp: Long,
|
||||
val type: String,
|
||||
val quote: Quote?,
|
||||
val attachments: MutableList<Attachment>,
|
||||
val profilePicture: ProfilePicture?,
|
||||
val signature: Signature?,
|
||||
val serverTimestamp: Long,
|
||||
) {
|
||||
|
||||
// region Settings
|
||||
companion object {
|
||||
fun from(message: VisibleMessage, server: String): OpenGroupMessage? {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val userPublicKey = storage.getUserPublicKey() ?: return null
|
||||
val attachmentIDs = message.attachmentIDs
|
||||
// Validation
|
||||
if (!message.isValid()) { return null } // Should be valid at this point
|
||||
// Quote
|
||||
val quote: Quote? = {
|
||||
val quote = message.quote
|
||||
if (quote != null && quote.isValid()) {
|
||||
val quotedMessageBody = quote.text ?: quote.timestamp!!.toString()
|
||||
val serverID = storage.getQuoteServerID(quote.timestamp!!, quote.publicKey!!)
|
||||
Quote(quote.timestamp!!, quote.publicKey!!, quotedMessageBody, serverID)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}()
|
||||
// Message
|
||||
val displayname = storage.getUserDisplayName() ?: "Anonymous"
|
||||
val text = message.text
|
||||
val body = if (text.isNullOrEmpty()) message.sentTimestamp.toString() else text // The back-end doesn't accept messages without a body so we use this as a workaround
|
||||
val result = OpenGroupMessage(null, userPublicKey, displayname, body, message.sentTimestamp!!, OpenGroupAPI.openGroupMessageType, quote, mutableListOf(), null, null, 0)
|
||||
// Link preview
|
||||
val linkPreview = message.linkPreview
|
||||
linkPreview?.let {
|
||||
if (!linkPreview.isValid()) { return@let }
|
||||
val attachmentID = linkPreview.attachmentID ?: return@let
|
||||
val attachment = MessagingModuleConfiguration.shared.messageDataProvider.getSignalAttachmentPointer(attachmentID) ?: return@let
|
||||
val openGroupLinkPreview = Attachment(
|
||||
Attachment.Kind.LinkPreview,
|
||||
server,
|
||||
attachment.id,
|
||||
attachment.contentType!!,
|
||||
attachment.size.get(),
|
||||
attachment.fileName.orNull(),
|
||||
0,
|
||||
attachment.width,
|
||||
attachment.height,
|
||||
attachment.caption.orNull(),
|
||||
attachment.url,
|
||||
linkPreview.url,
|
||||
linkPreview.title)
|
||||
result.attachments.add(openGroupLinkPreview)
|
||||
}
|
||||
// Attachments
|
||||
val attachments = message.attachmentIDs.mapNotNull {
|
||||
val attachment = MessagingModuleConfiguration.shared.messageDataProvider.getSignalAttachmentPointer(it) ?: return@mapNotNull null
|
||||
return@mapNotNull Attachment(
|
||||
Attachment.Kind.Attachment,
|
||||
server,
|
||||
attachment.id,
|
||||
attachment.contentType!!,
|
||||
attachment.size.orNull(),
|
||||
attachment.fileName.orNull() ?: "",
|
||||
0,
|
||||
attachment.width,
|
||||
attachment.height,
|
||||
attachment.caption.orNull(),
|
||||
attachment.url,
|
||||
null,
|
||||
null)
|
||||
}
|
||||
result.attachments.addAll(attachments)
|
||||
// Return
|
||||
return result
|
||||
}
|
||||
|
||||
private val curve = Curve25519.getInstance(Curve25519.BEST)
|
||||
private val signatureVersion: Long = 1
|
||||
private val attachmentType = "net.app.core.oembed"
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Types
|
||||
data class ProfilePicture(
|
||||
val profileKey: ByteArray,
|
||||
val url: String,
|
||||
)
|
||||
|
||||
data class Quote(
|
||||
val quotedMessageTimestamp: Long,
|
||||
val quoteePublicKey: String,
|
||||
val quotedMessageBody: String,
|
||||
val quotedMessageServerID: Long? = null,
|
||||
)
|
||||
|
||||
data class Signature(
|
||||
val data: ByteArray,
|
||||
val version: Long,
|
||||
)
|
||||
|
||||
data class Attachment(
|
||||
val kind: Kind,
|
||||
val server: String,
|
||||
val serverID: Long,
|
||||
val contentType: String,
|
||||
val size: Int,
|
||||
val fileName: String?,
|
||||
val flags: Int,
|
||||
val width: Int,
|
||||
val height: Int,
|
||||
val caption: String?,
|
||||
val url: String,
|
||||
/**
|
||||
Guaranteed to be non-`nil` if `kind` is `LinkPreview`.
|
||||
*/
|
||||
val linkPreviewURL: String?,
|
||||
/**
|
||||
Guaranteed to be non-`nil` if `kind` is `LinkPreview`.
|
||||
*/
|
||||
val linkPreviewTitle: String?,
|
||||
) {
|
||||
val dotNetAPIType = when {
|
||||
contentType.startsWith("image") -> "photo"
|
||||
contentType.startsWith("video") -> "video"
|
||||
contentType.startsWith("audio") -> "audio"
|
||||
else -> "other"
|
||||
}
|
||||
|
||||
enum class Kind(val rawValue: String) {
|
||||
Attachment("attachment"), LinkPreview("preview")
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Initialization
|
||||
constructor(hexEncodedPublicKey: String, displayName: String, body: String, timestamp: Long, type: String, quote: Quote?, attachments: List<Attachment>)
|
||||
: this(null, hexEncodedPublicKey, displayName, body, timestamp, type, quote, attachments as MutableList<Attachment>, null, null, 0)
|
||||
// endregion
|
||||
|
||||
// region Crypto
|
||||
internal fun sign(privateKey: ByteArray): OpenGroupMessage? {
|
||||
val data = getValidationData(signatureVersion)
|
||||
if (data == null) {
|
||||
Log.d("Loki", "Failed to sign public chat message.")
|
||||
return null
|
||||
}
|
||||
try {
|
||||
val signatureData = curve.calculateSignature(privateKey, data)
|
||||
val signature = Signature(signatureData, signatureVersion)
|
||||
return copy(signature = signature)
|
||||
} catch (e: Exception) {
|
||||
Log.d("Loki", "Failed to sign public chat message due to error: ${e.message}.")
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
internal fun hasValidSignature(): Boolean {
|
||||
if (signature == null) { return false }
|
||||
val data = getValidationData(signature.version) ?: return false
|
||||
val publicKey = Hex.fromStringCondensed(senderPublicKey.removing05PrefixIfNeeded())
|
||||
try {
|
||||
return curve.verifySignature(publicKey, data, signature.data)
|
||||
} catch (e: Exception) {
|
||||
Log.d("Loki", "Failed to verify public chat message due to error: ${e.message}.")
|
||||
return false
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Parsing
|
||||
internal fun toJSON(): Map<String, Any> {
|
||||
val value = mutableMapOf<String, Any>("timestamp" to timestamp)
|
||||
if (quote != null) {
|
||||
value["quote"] = mapOf("id" to quote.quotedMessageTimestamp, "author" to quote.quoteePublicKey, "text" to quote.quotedMessageBody)
|
||||
}
|
||||
if (signature != null) {
|
||||
value["sig"] = Hex.toStringCondensed(signature.data)
|
||||
value["sigver"] = signature.version
|
||||
}
|
||||
val annotation = mapOf("type" to type, "value" to value)
|
||||
val annotations = mutableListOf(annotation)
|
||||
attachments.forEach { attachment ->
|
||||
val attachmentValue = mutableMapOf(
|
||||
// Fields required by the .NET API
|
||||
"version" to 1,
|
||||
"type" to attachment.dotNetAPIType,
|
||||
// Custom fields
|
||||
"lokiType" to attachment.kind.rawValue,
|
||||
"server" to attachment.server,
|
||||
"id" to attachment.serverID,
|
||||
"contentType" to attachment.contentType,
|
||||
"size" to attachment.size,
|
||||
"fileName" to attachment.fileName,
|
||||
"flags" to attachment.flags,
|
||||
"width" to attachment.width,
|
||||
"height" to attachment.height,
|
||||
"url" to attachment.url
|
||||
)
|
||||
if (attachment.caption != null) { attachmentValue["caption"] = attachment.caption }
|
||||
if (attachment.linkPreviewURL != null) { attachmentValue["linkPreviewUrl"] = attachment.linkPreviewURL }
|
||||
if (attachment.linkPreviewTitle != null) { attachmentValue["linkPreviewTitle"] = attachment.linkPreviewTitle }
|
||||
val attachmentAnnotation = mapOf("type" to attachmentType, "value" to attachmentValue)
|
||||
annotations.add(attachmentAnnotation)
|
||||
}
|
||||
val result = mutableMapOf("text" to body, "annotations" to annotations)
|
||||
if (quote?.quotedMessageServerID != null) {
|
||||
result["reply_to"] = quote.quotedMessageServerID
|
||||
}
|
||||
return result
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Convenience
|
||||
private fun getValidationData(signatureVersion: Long): ByteArray? {
|
||||
var string = "${body.trim()}$timestamp"
|
||||
if (quote != null) {
|
||||
string += "${quote.quotedMessageTimestamp}${quote.quoteePublicKey}${quote.quotedMessageBody.trim()}"
|
||||
if (quote.quotedMessageServerID != null) {
|
||||
string += "${quote.quotedMessageServerID}"
|
||||
}
|
||||
}
|
||||
string += attachments.sortedBy { it.serverID }.map { it.serverID }.joinToString("")
|
||||
string += "$signatureVersion"
|
||||
try {
|
||||
return string.toByteArray(Charsets.UTF_8)
|
||||
} catch (exception: Exception) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
}
|
@@ -54,10 +54,11 @@ object MessageSender {
|
||||
|
||||
// Convenience
|
||||
fun send(message: Message, destination: Destination): Promise<Unit, Exception> {
|
||||
if (destination is Destination.OpenGroup || destination is Destination.OpenGroupV2) {
|
||||
if (destination is Destination.OpenGroupV2) {
|
||||
return sendToOpenGroupDestination(destination, message)
|
||||
} else {
|
||||
return sendToSnodeDestination(destination, message)
|
||||
}
|
||||
return sendToSnodeDestination(destination, message)
|
||||
}
|
||||
|
||||
// One-on-One Chats & Closed Groups
|
||||
@@ -84,7 +85,7 @@ object MessageSender {
|
||||
when (destination) {
|
||||
is Destination.Contact -> message.recipient = destination.publicKey
|
||||
is Destination.ClosedGroup -> message.recipient = destination.groupPublicKey
|
||||
is Destination.OpenGroup, is Destination.OpenGroupV2 -> throw IllegalStateException("Destination should not be an open group.")
|
||||
is Destination.OpenGroupV2 -> throw IllegalStateException("Destination should not be an open group.")
|
||||
}
|
||||
// Validate the message
|
||||
if (!message.isValid()) { throw Error.InvalidMessage }
|
||||
@@ -122,7 +123,7 @@ object MessageSender {
|
||||
val encryptionKeyPair = MessagingModuleConfiguration.shared.storage.getLatestClosedGroupEncryptionKeyPair(destination.groupPublicKey)!!
|
||||
ciphertext = MessageEncrypter.encrypt(plaintext, encryptionKeyPair.hexEncodedPublicKey)
|
||||
}
|
||||
is Destination.OpenGroup, is Destination.OpenGroupV2 -> throw IllegalStateException("Destination should not be open group.")
|
||||
is Destination.OpenGroupV2 -> throw IllegalStateException("Destination should not be open group.")
|
||||
}
|
||||
// Wrap the result
|
||||
val kind: SignalServiceProtos.Envelope.Type
|
||||
@@ -136,7 +137,7 @@ object MessageSender {
|
||||
kind = SignalServiceProtos.Envelope.Type.CLOSED_GROUP_MESSAGE
|
||||
senderPublicKey = destination.groupPublicKey
|
||||
}
|
||||
is Destination.OpenGroup, is Destination.OpenGroupV2 -> throw IllegalStateException("Destination should not be open group.")
|
||||
is Destination.OpenGroupV2 -> throw IllegalStateException("Destination should not be open group.")
|
||||
}
|
||||
val wrappedMessage = MessageWrapper.wrap(kind, message.sentTimestamp!!, senderPublicKey, ciphertext)
|
||||
// Send the result
|
||||
@@ -162,9 +163,11 @@ object MessageSender {
|
||||
}
|
||||
handleSuccessfulMessageSend(message, destination, isSyncMessage)
|
||||
var shouldNotify = (message is VisibleMessage && !isSyncMessage)
|
||||
/*
|
||||
if (message is ClosedGroupControlMessage && message.kind is ClosedGroupControlMessage.Kind.New) {
|
||||
shouldNotify = true
|
||||
}
|
||||
*/
|
||||
if (shouldNotify) {
|
||||
val notifyPNServerJob = NotifyPNServerJob(snodeMessage)
|
||||
JobQueue.shared.add(notifyPNServerJob)
|
||||
@@ -203,27 +206,6 @@ object MessageSender {
|
||||
try {
|
||||
when (destination) {
|
||||
is Destination.Contact, is Destination.ClosedGroup -> throw IllegalStateException("Invalid destination.")
|
||||
is Destination.OpenGroup -> {
|
||||
message.recipient = "${destination.server}.${destination.channel}"
|
||||
val server = destination.server
|
||||
val channel = destination.channel
|
||||
// Validate the message
|
||||
if (message !is VisibleMessage || !message.isValid()) {
|
||||
throw Error.InvalidMessage
|
||||
}
|
||||
// Convert the message to an open group message
|
||||
val openGroupMessage = OpenGroupMessage.from(message, server) ?: run {
|
||||
throw Error.InvalidMessage
|
||||
}
|
||||
// Send the result
|
||||
OpenGroupAPI.sendMessage(openGroupMessage, channel, server).success {
|
||||
message.openGroupServerMessageID = it.serverID
|
||||
handleSuccessfulMessageSend(message, destination)
|
||||
deferred.resolve(Unit)
|
||||
}.fail {
|
||||
handleFailure(it)
|
||||
}
|
||||
}
|
||||
is Destination.OpenGroupV2 -> {
|
||||
message.recipient = "${destination.server}.${destination.room}"
|
||||
val server = destination.server
|
||||
@@ -275,7 +257,7 @@ object MessageSender {
|
||||
// Track the open group server message ID
|
||||
if (message.openGroupServerMessageID != null && destination is Destination.OpenGroupV2) {
|
||||
val encoded = GroupUtil.getEncodedOpenGroupID("${destination.server}.${destination.room}".toByteArray())
|
||||
val threadID = storage.getThreadIdFor(Address.fromSerialized(encoded))
|
||||
val threadID = storage.getThreadId(Address.fromSerialized(encoded))
|
||||
if (threadID != null && threadID >= 0) {
|
||||
storage.setOpenGroupServerMessageID(messageID, message.openGroupServerMessageID!!, threadID, !(message as VisibleMessage).isMediaMessage())
|
||||
}
|
||||
|
@@ -192,8 +192,8 @@ fun MessageSender.removeMembers(groupPublicKey: String, membersToRemove: List<St
|
||||
// Save the new group members
|
||||
storage.updateMembers(groupID, updatedMembers.map { Address.fromSerialized(it) })
|
||||
// Update the zombie list
|
||||
val oldZombies = storage.getZombieMember(groupID)
|
||||
storage.updateZombieMembers(groupID, oldZombies.minus(membersToRemove).map { Address.fromSerialized(it) })
|
||||
val oldZombies = storage.getZombieMembers(groupID)
|
||||
storage.setZombieMembers(groupID, oldZombies.minus(membersToRemove).map { Address.fromSerialized(it) })
|
||||
val removeMembersAsData = membersToRemove.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) }
|
||||
val name = group.title
|
||||
// Send the update to the group
|
||||
|
@@ -69,21 +69,21 @@ private fun MessageReceiver.handleTypingIndicator(message: TypingIndicator) {
|
||||
fun MessageReceiver.showTypingIndicatorIfNeeded(senderPublicKey: String) {
|
||||
val context = MessagingModuleConfiguration.shared.context
|
||||
val address = Address.fromSerialized(senderPublicKey)
|
||||
val threadID = MessagingModuleConfiguration.shared.storage.getThreadIdFor(address) ?: return
|
||||
val threadID = MessagingModuleConfiguration.shared.storage.getThreadId(address) ?: return
|
||||
SSKEnvironment.shared.typingIndicators.didReceiveTypingStartedMessage(context, threadID, address, 1)
|
||||
}
|
||||
|
||||
fun MessageReceiver.hideTypingIndicatorIfNeeded(senderPublicKey: String) {
|
||||
val context = MessagingModuleConfiguration.shared.context
|
||||
val address = Address.fromSerialized(senderPublicKey)
|
||||
val threadID = MessagingModuleConfiguration.shared.storage.getThreadIdFor(address) ?: return
|
||||
val threadID = MessagingModuleConfiguration.shared.storage.getThreadId(address) ?: return
|
||||
SSKEnvironment.shared.typingIndicators.didReceiveTypingStoppedMessage(context, threadID, address, 1, false)
|
||||
}
|
||||
|
||||
fun MessageReceiver.cancelTypingIndicatorsIfNeeded(senderPublicKey: String) {
|
||||
val context = MessagingModuleConfiguration.shared.context
|
||||
val address = Address.fromSerialized(senderPublicKey)
|
||||
val threadID = MessagingModuleConfiguration.shared.storage.getThreadIdFor(address) ?: return
|
||||
val threadID = MessagingModuleConfiguration.shared.storage.getThreadId(address) ?: return
|
||||
SSKEnvironment.shared.typingIndicators.didReceiveIncomingMessage(context, threadID, address, 1)
|
||||
}
|
||||
|
||||
@@ -123,11 +123,10 @@ private fun handleConfigurationMessage(message: ConfigurationMessage) {
|
||||
handleNewClosedGroup(message.sender!!, message.sentTimestamp!!, closedGroup.publicKey, closedGroup.name,
|
||||
closedGroup.encryptionKeyPair!!, closedGroup.members, closedGroup.admins, message.sentTimestamp!!)
|
||||
}
|
||||
val allOpenGroups = storage.getAllOpenGroups().map { it.value.server }
|
||||
val allV2OpenGroups = storage.getAllV2OpenGroups().map { it.value.joinURL }
|
||||
for (openGroup in message.openGroups) {
|
||||
if (allOpenGroups.contains(openGroup) || allV2OpenGroups.contains(openGroup)) continue
|
||||
storage.addOpenGroup(openGroup, 1)
|
||||
if (allV2OpenGroups.contains(openGroup)) continue
|
||||
storage.addOpenGroup(openGroup)
|
||||
}
|
||||
if (message.displayName.isNotEmpty()) {
|
||||
TextSecurePreferences.setProfileName(context, message.displayName)
|
||||
@@ -138,7 +137,7 @@ private fun handleConfigurationMessage(message: ConfigurationMessage) {
|
||||
val profileKey = Base64.encodeBytes(message.profileKey)
|
||||
ProfileKeyUtil.setEncodedProfileKey(context, profileKey)
|
||||
storage.setProfileKeyForRecipient(userPublicKey, message.profileKey)
|
||||
storage.setUserProfilePictureUrl(message.profilePicture!!)
|
||||
storage.setUserProfilePictureURL(message.profilePicture!!)
|
||||
}
|
||||
storage.addContacts(message.contacts)
|
||||
}
|
||||
@@ -158,12 +157,9 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, proto: SignalS
|
||||
// Thread doesn't exist; should only be reached in a case where we are processing open group messages for a no longer existent thread
|
||||
throw MessageReceiver.Error.NoThread
|
||||
}
|
||||
val openGroup = threadID.let {
|
||||
storage.getOpenGroup(it.toString())
|
||||
}
|
||||
// Update profile if needed
|
||||
val profile = message.profile
|
||||
if (profile != null && userPublicKey != message.sender && openGroup == null) { // Don't do this in V1 open groups
|
||||
if (profile != null && userPublicKey != message.sender) {
|
||||
val profileManager = SSKEnvironment.shared.profileManager
|
||||
val recipient = Recipient.from(context, Address.fromSerialized(message.sender!!), false)
|
||||
val displayName = profile.displayName!!
|
||||
@@ -475,8 +471,8 @@ private fun MessageReceiver.handleClosedGroupMembersRemoved(message: ClosedGroup
|
||||
storage.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) })
|
||||
}
|
||||
// Update zombie members
|
||||
val zombies = storage.getZombieMember(groupID)
|
||||
storage.updateZombieMembers(groupID, zombies.minus(removedMembers).map { Address.fromSerialized(it) })
|
||||
val zombies = storage.getZombieMembers(groupID)
|
||||
storage.setZombieMembers(groupID, zombies.minus(removedMembers).map { Address.fromSerialized(it) })
|
||||
val type = if (senderLeft) SignalServiceGroup.Type.QUIT else SignalServiceGroup.Type.MEMBER_REMOVED
|
||||
// Notify the user
|
||||
// We don't display zombie members in the notification as users have already been notified when those members left
|
||||
@@ -528,8 +524,8 @@ private fun MessageReceiver.handleClosedGroupMemberLeft(message: ClosedGroupCont
|
||||
} else {
|
||||
storage.updateMembers(groupID, updatedMemberList.map { Address.fromSerialized(it) })
|
||||
// Update zombie members
|
||||
val zombies = storage.getZombieMember(groupID)
|
||||
storage.updateZombieMembers(groupID, zombies.plus(senderPublicKey).map { Address.fromSerialized(it) })
|
||||
val zombies = storage.getZombieMembers(groupID)
|
||||
storage.setZombieMembers(groupID, zombies.plus(senderPublicKey).map { Address.fromSerialized(it) })
|
||||
}
|
||||
// Notify the user
|
||||
if (userLeft) {
|
||||
|
@@ -28,7 +28,7 @@ class ClosedGroupPollerV2 {
|
||||
|
||||
companion object {
|
||||
private val minPollInterval = 4 * 1000
|
||||
private val maxPollInterval = 2 * 60 * 1000
|
||||
private val maxPollInterval = 4 * 60 * 1000
|
||||
|
||||
@JvmStatic
|
||||
val shared = ClosedGroupPollerV2()
|
||||
@@ -75,7 +75,7 @@ class ClosedGroupPollerV2 {
|
||||
// reasonable fake time interval to use instead.
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||
val threadID = storage.getThreadID(groupID)?.toLongOrNull() ?: return
|
||||
val threadID = storage.getThreadId(groupID) ?: return
|
||||
val lastUpdated = storage.getLastUpdated(threadID)
|
||||
val timeSinceLastMessage = if (lastUpdated != -1L) Date().time - lastUpdated else 5 * 60 * 1000
|
||||
val minPollInterval = Companion.minPollInterval
|
||||
|
@@ -1,232 +0,0 @@
|
||||
package org.session.libsession.messaging.sending_receiving.pollers
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.deferred
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.jobs.JobQueue
|
||||
import org.session.libsession.messaging.jobs.MessageReceiveJob
|
||||
import org.session.libsession.messaging.open_groups.OpenGroup
|
||||
import org.session.libsession.messaging.open_groups.OpenGroupAPI
|
||||
import org.session.libsession.messaging.open_groups.OpenGroupMessage
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsignal.protos.SignalServiceProtos.*
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.session.libsignal.utilities.successBackground
|
||||
import java.util.*
|
||||
import java.util.concurrent.ScheduledExecutorService
|
||||
import java.util.concurrent.ScheduledFuture
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class OpenGroupPoller(private val openGroup: OpenGroup, private val executorService: ScheduledExecutorService? = null) {
|
||||
|
||||
private var hasStarted = false
|
||||
@Volatile private var isPollOngoing = false
|
||||
var isCaughtUp = false
|
||||
|
||||
private val cancellableFutures = mutableListOf<ScheduledFuture<out Any>>()
|
||||
|
||||
// region Convenience
|
||||
private val userHexEncodedPublicKey = MessagingModuleConfiguration.shared.storage.getUserPublicKey() ?: ""
|
||||
private var displayNameUpdates = setOf<String>()
|
||||
// endregion
|
||||
|
||||
// region Settings
|
||||
companion object {
|
||||
private val pollForNewMessagesInterval: Long = 10 * 1000
|
||||
private val pollForDeletedMessagesInterval: Long = 60 * 1000
|
||||
private val pollForModeratorsInterval: Long = 10 * 60 * 1000
|
||||
private val pollForDisplayNamesInterval: Long = 60 * 1000
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Lifecycle
|
||||
fun startIfNeeded() {
|
||||
if (hasStarted || executorService == null) return
|
||||
cancellableFutures += listOf(
|
||||
executorService.scheduleAtFixedRate(::pollForNewMessages,0, pollForNewMessagesInterval, TimeUnit.MILLISECONDS),
|
||||
executorService.scheduleAtFixedRate(::pollForDeletedMessages,0, pollForDeletedMessagesInterval, TimeUnit.MILLISECONDS),
|
||||
executorService.scheduleAtFixedRate(::pollForModerators,0, pollForModeratorsInterval, TimeUnit.MILLISECONDS),
|
||||
executorService.scheduleAtFixedRate(::pollForDisplayNames,0, pollForDisplayNamesInterval, TimeUnit.MILLISECONDS)
|
||||
)
|
||||
hasStarted = true
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
cancellableFutures.forEach { future ->
|
||||
future.cancel(false)
|
||||
}
|
||||
cancellableFutures.clear()
|
||||
hasStarted = false
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Polling
|
||||
fun pollForNewMessages(): Promise<Unit, Exception> {
|
||||
return pollForNewMessagesInternal(false)
|
||||
}
|
||||
|
||||
private fun pollForNewMessagesInternal(isBackgroundPoll: Boolean): Promise<Unit, Exception> {
|
||||
if (isPollOngoing) { return Promise.of(Unit) }
|
||||
isPollOngoing = true
|
||||
val deferred = deferred<Unit, Exception>()
|
||||
// Kovenant propagates a context to chained promises, so OpenGroupAPI.sharedContext should be used for all of the below
|
||||
OpenGroupAPI.getMessages(openGroup.channel, openGroup.server).successBackground { messages ->
|
||||
// Process messages in the background
|
||||
messages.forEach { message ->
|
||||
try {
|
||||
val senderPublicKey = message.senderPublicKey
|
||||
fun generateDisplayName(rawDisplayName: String): String {
|
||||
return "$rawDisplayName (...${senderPublicKey.takeLast(8)})"
|
||||
}
|
||||
val senderDisplayName = MessagingModuleConfiguration.shared.storage.getOpenGroupDisplayName(senderPublicKey, openGroup.channel, openGroup.server) ?: generateDisplayName(message.displayName)
|
||||
val id = openGroup.id.toByteArray()
|
||||
// Main message
|
||||
val dataMessageProto = DataMessage.newBuilder()
|
||||
val body = if (message.body == message.timestamp.toString()) { "" } else { message.body }
|
||||
dataMessageProto.setBody(body)
|
||||
dataMessageProto.setTimestamp(message.timestamp)
|
||||
// Attachments
|
||||
val attachmentProtos = message.attachments.mapNotNull { attachment ->
|
||||
try {
|
||||
if (attachment.kind != OpenGroupMessage.Attachment.Kind.Attachment) { return@mapNotNull null }
|
||||
val attachmentProto = AttachmentPointer.newBuilder()
|
||||
attachmentProto.setId(attachment.serverID)
|
||||
attachmentProto.setContentType(attachment.contentType)
|
||||
attachmentProto.setSize(attachment.size)
|
||||
attachmentProto.setFileName(attachment.fileName)
|
||||
attachmentProto.setFlags(attachment.flags)
|
||||
attachmentProto.setWidth(attachment.width)
|
||||
attachmentProto.setHeight(attachment.height)
|
||||
attachment.caption?.let { attachmentProto.setCaption(it) }
|
||||
attachmentProto.setUrl(attachment.url)
|
||||
attachmentProto.build()
|
||||
} catch (e: Exception) {
|
||||
Log.e("Loki","Failed to parse attachment as proto",e)
|
||||
null
|
||||
}
|
||||
}
|
||||
dataMessageProto.addAllAttachments(attachmentProtos)
|
||||
// Link preview
|
||||
val linkPreview = message.attachments.firstOrNull { it.kind == OpenGroupMessage.Attachment.Kind.LinkPreview }
|
||||
if (linkPreview != null) {
|
||||
val linkPreviewProto = DataMessage.Preview.newBuilder()
|
||||
linkPreviewProto.setUrl(linkPreview.linkPreviewURL!!)
|
||||
linkPreviewProto.setTitle(linkPreview.linkPreviewTitle!!)
|
||||
val attachmentProto = AttachmentPointer.newBuilder()
|
||||
attachmentProto.setId(linkPreview.serverID)
|
||||
attachmentProto.setContentType(linkPreview.contentType)
|
||||
attachmentProto.setSize(linkPreview.size)
|
||||
attachmentProto.setFileName(linkPreview.fileName)
|
||||
attachmentProto.setFlags(linkPreview.flags)
|
||||
attachmentProto.setWidth(linkPreview.width)
|
||||
attachmentProto.setHeight(linkPreview.height)
|
||||
linkPreview.caption?.let { attachmentProto.setCaption(it) }
|
||||
attachmentProto.setUrl(linkPreview.url)
|
||||
linkPreviewProto.setImage(attachmentProto.build())
|
||||
dataMessageProto.addPreview(linkPreviewProto.build())
|
||||
}
|
||||
// Quote
|
||||
val quote = message.quote
|
||||
if (quote != null) {
|
||||
val quoteProto = DataMessage.Quote.newBuilder()
|
||||
quoteProto.setId(quote.quotedMessageTimestamp)
|
||||
quoteProto.setAuthor(quote.quoteePublicKey)
|
||||
if (quote.quotedMessageBody != quote.quotedMessageTimestamp.toString()) { quoteProto.setText(quote.quotedMessageBody) }
|
||||
dataMessageProto.setQuote(quoteProto.build())
|
||||
}
|
||||
val messageServerID = message.serverID
|
||||
// Profile
|
||||
val profileProto = DataMessage.LokiProfile.newBuilder()
|
||||
profileProto.setDisplayName(senderDisplayName)
|
||||
val profilePicture = message.profilePicture
|
||||
if (profilePicture != null) {
|
||||
profileProto.setProfilePicture(profilePicture.url)
|
||||
dataMessageProto.setProfileKey(ByteString.copyFrom(profilePicture.profileKey))
|
||||
}
|
||||
dataMessageProto.setProfile(profileProto.build())
|
||||
/* TODO: the signal service proto needs to be synced with iOS
|
||||
// Open group info
|
||||
if (messageServerID != null) {
|
||||
val openGroupProto = PublicChatInfo.newBuilder()
|
||||
openGroupProto.setServerID(messageServerID)
|
||||
dataMessageProto.setPublicChatInfo(openGroupProto.build())
|
||||
}
|
||||
*/
|
||||
// Signal group context
|
||||
val groupProto = GroupContext.newBuilder()
|
||||
groupProto.setId(ByteString.copyFrom(id))
|
||||
groupProto.setType(GroupContext.Type.DELIVER)
|
||||
groupProto.setName(openGroup.displayName)
|
||||
dataMessageProto.setGroup(groupProto.build())
|
||||
// Content
|
||||
val content = Content.newBuilder()
|
||||
content.setDataMessage(dataMessageProto.build())
|
||||
// Envelope
|
||||
val builder = Envelope.newBuilder()
|
||||
builder.type = Envelope.Type.SESSION_MESSAGE
|
||||
builder.source = senderPublicKey
|
||||
builder.sourceDevice = 1
|
||||
builder.setContent(content.build().toByteString())
|
||||
builder.timestamp = message.timestamp
|
||||
builder.serverTimestamp = message.serverTimestamp
|
||||
val envelope = builder.build()
|
||||
val job = MessageReceiveJob(envelope.toByteArray(), messageServerID, openGroup.id)
|
||||
Log.d("Loki", "Scheduling Job $job")
|
||||
if (isBackgroundPoll) {
|
||||
job.executeAsync().always { deferred.resolve(Unit) }
|
||||
// The promise is just used to keep track of when we're done
|
||||
} else {
|
||||
JobQueue.shared.add(job)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("Loki", "Exception parsing message", e)
|
||||
}
|
||||
}
|
||||
displayNameUpdates = displayNameUpdates + messages.map { it.senderPublicKey }.toSet() - userHexEncodedPublicKey
|
||||
executorService?.schedule(::pollForDisplayNames, 0, TimeUnit.MILLISECONDS)
|
||||
isCaughtUp = true
|
||||
isPollOngoing = false
|
||||
deferred.resolve(Unit)
|
||||
}.fail {
|
||||
Log.d("Loki", "Failed to get messages for group chat with ID: ${openGroup.channel} on server: ${openGroup.server}.")
|
||||
isPollOngoing = false
|
||||
}
|
||||
return deferred.promise
|
||||
}
|
||||
|
||||
private fun pollForDisplayNames() {
|
||||
if (displayNameUpdates.isEmpty()) { return }
|
||||
val hexEncodedPublicKeys = displayNameUpdates
|
||||
displayNameUpdates = setOf()
|
||||
OpenGroupAPI.getDisplayNames(hexEncodedPublicKeys, openGroup.server).successBackground { mapping ->
|
||||
for (pair in mapping.entries) {
|
||||
if (pair.key == userHexEncodedPublicKey) continue
|
||||
val senderDisplayName = "${pair.value} (...${pair.key.substring(pair.key.count() - 8)})"
|
||||
MessagingModuleConfiguration.shared.storage.setOpenGroupDisplayName(pair.key, openGroup.channel, openGroup.server, senderDisplayName)
|
||||
}
|
||||
}.fail {
|
||||
displayNameUpdates = displayNameUpdates.union(hexEncodedPublicKeys)
|
||||
}
|
||||
}
|
||||
|
||||
private fun pollForDeletedMessages() {
|
||||
val messagingModule = MessagingModuleConfiguration.shared
|
||||
val address = GroupUtil.getEncodedOpenGroupID(openGroup.id.toByteArray())
|
||||
val threadId = messagingModule.storage.getThreadIdFor(Address.fromSerialized(address)) ?: return
|
||||
OpenGroupAPI.getDeletedMessageServerIDs(openGroup.channel, openGroup.server).success { deletedMessageServerIDs ->
|
||||
val deletedMessageIDs = deletedMessageServerIDs.mapNotNull { messagingModule.messageDataProvider.getMessageID(it, threadId) }
|
||||
deletedMessageIDs.forEach { (messageId, isSms) ->
|
||||
MessagingModuleConfiguration.shared.messageDataProvider.deleteMessage(messageId, isSms)
|
||||
}
|
||||
}.fail {
|
||||
Log.d("Loki", "Failed to get deleted messages for group chat with ID: ${openGroup.channel} on server: ${openGroup.server}.")
|
||||
}
|
||||
}
|
||||
|
||||
private fun pollForModerators() {
|
||||
OpenGroupAPI.getModerators(openGroup.channel, openGroup.server)
|
||||
}
|
||||
// endregion
|
||||
}
|
@@ -78,7 +78,7 @@ class OpenGroupPollerV2(private val server: String, private val executorService:
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val dataProvider = MessagingModuleConfiguration.shared.messageDataProvider
|
||||
val groupID = GroupUtil.getEncodedOpenGroupID(openGroupID.toByteArray())
|
||||
val threadID = storage.getThreadIdFor(Address.fromSerialized(groupID)) ?: return
|
||||
val threadID = storage.getThreadId(Address.fromSerialized(groupID)) ?: return
|
||||
val deletedMessageIDs = deletedMessageServerIDs.mapNotNull { serverID ->
|
||||
val messageID = dataProvider.getMessageID(serverID, threadID)
|
||||
if (messageID == null) {
|
||||
|
@@ -1,269 +0,0 @@
|
||||
package org.session.libsession.messaging.utilities
|
||||
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.functional.bind
|
||||
import nl.komponents.kovenant.functional.map
|
||||
import nl.komponents.kovenant.then
|
||||
import okhttp3.*
|
||||
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.snode.OnionRequestAPI
|
||||
import org.session.libsession.messaging.file_server.FileServerAPI
|
||||
|
||||
import org.session.libsignal.crypto.DiffieHellman
|
||||
import org.session.libsignal.streams.ProfileCipherOutputStream
|
||||
import org.session.libsignal.exceptions.NonSuccessfulResponseCodeException
|
||||
import org.session.libsignal.exceptions.PushNetworkException
|
||||
import org.session.libsignal.streams.StreamDetails
|
||||
import org.session.libsignal.utilities.ProfileAvatarData
|
||||
import org.session.libsignal.utilities.PushAttachmentData
|
||||
import org.session.libsignal.streams.DigestingRequestBody
|
||||
import org.session.libsignal.streams.ProfileCipherOutputStreamFactory
|
||||
import org.session.libsignal.utilities.Hex
|
||||
import org.session.libsignal.utilities.JsonUtil
|
||||
import org.session.libsignal.utilities.HTTP
|
||||
import org.session.libsignal.utilities.*
|
||||
import org.session.libsignal.utilities.Base64
|
||||
import org.session.libsignal.utilities.Log
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Base class that provides utilities for .NET based APIs.
|
||||
*/
|
||||
open class DotNetAPI {
|
||||
|
||||
internal enum class HTTPVerb { GET, PUT, POST, DELETE, PATCH }
|
||||
|
||||
// Error
|
||||
internal sealed class Error(val description: String) : Exception(description) {
|
||||
object Generic : Error("An error occurred.")
|
||||
object InvalidURL : Error("Invalid URL.")
|
||||
object ParsingFailed : Error("Invalid file server response.")
|
||||
object SigningFailed : Error("Couldn't sign message.")
|
||||
object EncryptionFailed : Error("Couldn't encrypt file.")
|
||||
object DecryptionFailed : Error("Couldn't decrypt file.")
|
||||
object MaxFileSizeExceeded : Error("Maximum file size exceeded.")
|
||||
object TokenExpired: Error("Token expired.") // Session Android
|
||||
|
||||
internal val isRetryable: Boolean = false
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val authTokenRequestCache = hashMapOf<String, Promise<String, Exception>>()
|
||||
}
|
||||
|
||||
public data class UploadResult(val id: Long, val url: String, val digest: ByteArray?)
|
||||
|
||||
fun getAuthToken(server: String): Promise<String, Exception> {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val token = storage.getAuthToken(server)
|
||||
if (token != null) { return Promise.of(token) }
|
||||
// Avoid multiple token requests to the server by caching
|
||||
var promise = authTokenRequestCache[server]
|
||||
if (promise == null) {
|
||||
promise = requestNewAuthToken(server).bind { submitAuthToken(it, server) }.then { newToken ->
|
||||
storage.setAuthToken(server, newToken)
|
||||
newToken
|
||||
}.always {
|
||||
authTokenRequestCache.remove(server)
|
||||
}
|
||||
authTokenRequestCache[server] = promise
|
||||
}
|
||||
return promise
|
||||
}
|
||||
|
||||
private fun requestNewAuthToken(server: String): Promise<String, Exception> {
|
||||
Log.d("Loki", "Requesting auth token for server: $server.")
|
||||
val userKeyPair = MessagingModuleConfiguration.shared.storage.getUserKeyPair() ?: throw Error.Generic
|
||||
val parameters: Map<String, Any> = mapOf( "pubKey" to userKeyPair.first )
|
||||
return execute(HTTPVerb.GET, server, "loki/v1/get_challenge", false, parameters).map { json ->
|
||||
try {
|
||||
val base64EncodedChallenge = json["cipherText64"] as String
|
||||
val challenge = Base64.decode(base64EncodedChallenge)
|
||||
val base64EncodedServerPublicKey = json["serverPubKey64"] as String
|
||||
var serverPublicKey = Base64.decode(base64EncodedServerPublicKey)
|
||||
// Discard the "05" prefix if needed
|
||||
if (serverPublicKey.count() == 33) {
|
||||
val hexEncodedServerPublicKey = Hex.toStringCondensed(serverPublicKey)
|
||||
serverPublicKey = Hex.fromStringCondensed(hexEncodedServerPublicKey.removing05PrefixIfNeeded())
|
||||
}
|
||||
// The challenge is prefixed by the 16 bit IV
|
||||
val tokenAsData = DiffieHellman.decrypt(challenge, serverPublicKey, userKeyPair.second)
|
||||
val token = tokenAsData.toString(Charsets.UTF_8)
|
||||
token
|
||||
} catch (exception: Exception) {
|
||||
Log.d("Loki", "Couldn't parse auth token for server: $server.")
|
||||
throw exception
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun submitAuthToken(token: String, server: String): Promise<String, Exception> {
|
||||
Log.d("Loki", "Submitting auth token for server: $server.")
|
||||
val userPublicKey = MessagingModuleConfiguration.shared.storage.getUserPublicKey() ?: throw Error.Generic
|
||||
val parameters = mapOf( "pubKey" to userPublicKey, "token" to token )
|
||||
return execute(HTTPVerb.POST, server, "loki/v1/submit_challenge", false, parameters, isJSONRequired = false).map { token }
|
||||
}
|
||||
|
||||
internal fun execute(verb: HTTPVerb, server: String, endpoint: String, isAuthRequired: Boolean = true, parameters: Map<String, Any> = mapOf(), isJSONRequired: Boolean = true): Promise<Map<*, *>, Exception> {
|
||||
fun execute(token: String?): Promise<Map<*, *>, Exception> {
|
||||
val sanitizedEndpoint = endpoint.removePrefix("/")
|
||||
var url = "$server/$sanitizedEndpoint"
|
||||
if (verb == HTTPVerb.GET || verb == HTTPVerb.DELETE) {
|
||||
val queryParameters = parameters.map { "${it.key}=${it.value}" }.joinToString("&")
|
||||
if (queryParameters.isNotEmpty()) { url += "?$queryParameters" }
|
||||
}
|
||||
var request = Request.Builder().url(url)
|
||||
if (isAuthRequired) {
|
||||
if (token == null) { throw IllegalStateException() }
|
||||
request = request.header("Authorization", "Bearer $token")
|
||||
}
|
||||
when (verb) {
|
||||
HTTPVerb.GET -> request = request.get()
|
||||
HTTPVerb.DELETE -> request = request.delete()
|
||||
else -> {
|
||||
val parametersAsJSON = JsonUtil.toJson(parameters)
|
||||
val body = RequestBody.create(MediaType.get("application/json"), parametersAsJSON)
|
||||
when (verb) {
|
||||
HTTPVerb.PUT -> request = request.put(body)
|
||||
HTTPVerb.POST -> request = request.post(body)
|
||||
HTTPVerb.PATCH -> request = request.patch(body)
|
||||
else -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
}
|
||||
val serverPublicKeyPromise = if (server == FileServerAPI.shared.server) Promise.of(FileServerAPI.fileServerPublicKey)
|
||||
else FileServerAPI.shared.getPublicKeyForOpenGroupServer(server)
|
||||
return serverPublicKeyPromise.bind { serverPublicKey ->
|
||||
OnionRequestAPI.sendOnionRequest(request.build(), server, serverPublicKey, isJSONRequired = isJSONRequired).recover { exception ->
|
||||
if (exception is HTTP.HTTPRequestFailedException) {
|
||||
val statusCode = exception.statusCode
|
||||
if (statusCode == 401 || statusCode == 403) {
|
||||
MessagingModuleConfiguration.shared.storage.setAuthToken(server, null)
|
||||
throw Error.TokenExpired
|
||||
}
|
||||
}
|
||||
throw exception
|
||||
}
|
||||
}
|
||||
}
|
||||
return if (isAuthRequired) {
|
||||
getAuthToken(server).bind { execute(it) }
|
||||
} else {
|
||||
execute(null)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun getUserProfiles(publicKeys: Set<String>, server: String, includeAnnotations: Boolean): Promise<List<Map<*, *>>, Exception> {
|
||||
val parameters = mapOf( "include_user_annotations" to includeAnnotations.toInt(), "ids" to publicKeys.joinToString { "@$it" } )
|
||||
return execute(HTTPVerb.GET, server, "users", parameters = parameters).map { json ->
|
||||
val data = json["data"] as? List<Map<*, *>>
|
||||
if (data == null) {
|
||||
Log.d("Loki", "Couldn't parse user profiles for: $publicKeys from: $json.")
|
||||
throw Error.ParsingFailed
|
||||
}
|
||||
data!! // For some reason the compiler can't infer that this can't be null at this point
|
||||
}
|
||||
}
|
||||
|
||||
internal fun setSelfAnnotation(server: String, type: String, newValue: Any?): Promise<Map<*, *>, Exception> {
|
||||
val annotation = mutableMapOf<String, Any>( "type" to type )
|
||||
if (newValue != null) { annotation["value"] = newValue }
|
||||
val parameters = mapOf( "annotations" to listOf( annotation ) )
|
||||
return execute(HTTPVerb.PATCH, server, "users/me", parameters = parameters)
|
||||
}
|
||||
|
||||
// UPLOAD
|
||||
|
||||
// TODO: migrate to v2 file server
|
||||
@Throws(PushNetworkException::class, NonSuccessfulResponseCodeException::class)
|
||||
fun uploadAttachment(server: String, attachment: PushAttachmentData): UploadResult {
|
||||
// This function mimics what Signal does in PushServiceSocket
|
||||
val contentType = "application/octet-stream"
|
||||
val file = DigestingRequestBody(attachment.data, attachment.outputStreamFactory, contentType, attachment.dataSize, attachment.listener)
|
||||
Log.d("Loki", "File size: ${attachment.dataSize} bytes.")
|
||||
val body = MultipartBody.Builder()
|
||||
.setType(MultipartBody.FORM)
|
||||
.addFormDataPart("type", "network.loki")
|
||||
.addFormDataPart("Content-Type", contentType)
|
||||
.addFormDataPart("content", UUID.randomUUID().toString(), file)
|
||||
.build()
|
||||
val request = Request.Builder().url("$server/files").post(body)
|
||||
return upload(server, request) { json -> // Retrying is handled by AttachmentUploadJob
|
||||
val data = json["data"] as? Map<*, *>
|
||||
if (data == null) {
|
||||
Log.e("Loki", "Couldn't parse attachment from: $json.")
|
||||
throw Error.ParsingFailed
|
||||
}
|
||||
val id = data["id"] as? Long ?: (data["id"] as? Int)?.toLong() ?: (data["id"] as? String)?.toLong()
|
||||
val url = data["url"] as? String
|
||||
if (id == null || url == null || url.isEmpty()) {
|
||||
Log.e("Loki", "Couldn't parse upload from: $json.")
|
||||
throw Error.ParsingFailed
|
||||
}
|
||||
UploadResult(id, url, file.transmittedDigest)
|
||||
}.get()
|
||||
}
|
||||
|
||||
@Throws(PushNetworkException::class, NonSuccessfulResponseCodeException::class)
|
||||
fun uploadProfilePicture(server: String, key: ByteArray, profilePicture: StreamDetails, setLastProfilePictureUpload: () -> Unit): UploadResult {
|
||||
val profilePictureUploadData = ProfileAvatarData(profilePicture.stream, ProfileCipherOutputStream.getCiphertextLength(profilePicture.length), profilePicture.contentType, ProfileCipherOutputStreamFactory(key))
|
||||
val file = DigestingRequestBody(profilePictureUploadData.data, profilePictureUploadData.outputStreamFactory,
|
||||
profilePictureUploadData.contentType, profilePictureUploadData.dataLength, null)
|
||||
val body = MultipartBody.Builder()
|
||||
.setType(MultipartBody.FORM)
|
||||
.addFormDataPart("type", "network.loki")
|
||||
.addFormDataPart("Content-Type", "application/octet-stream")
|
||||
.addFormDataPart("content", UUID.randomUUID().toString(), file)
|
||||
.build()
|
||||
val request = Request.Builder().url("$server/files").post(body)
|
||||
return retryIfNeeded(4) {
|
||||
upload(server, request) { json ->
|
||||
val data = json["data"] as? Map<*, *>
|
||||
if (data == null) {
|
||||
Log.d("Loki", "Couldn't parse profile picture from: $json.")
|
||||
throw Error.ParsingFailed
|
||||
}
|
||||
val id = data["id"] as? Long ?: (data["id"] as? Int)?.toLong() ?: (data["id"] as? String)?.toLong()
|
||||
val url = data["url"] as? String
|
||||
if (id == null || url == null || url.isEmpty()) {
|
||||
Log.d("Loki", "Couldn't parse profile picture from: $json.")
|
||||
throw Error.ParsingFailed
|
||||
}
|
||||
setLastProfilePictureUpload()
|
||||
UploadResult(id, url, file.transmittedDigest)
|
||||
}
|
||||
}.get()
|
||||
}
|
||||
|
||||
@Throws(PushNetworkException::class, NonSuccessfulResponseCodeException::class)
|
||||
private fun upload(server: String, request: Request.Builder, parse: (Map<*, *>) -> UploadResult): Promise<UploadResult, Exception> {
|
||||
val promise: Promise<Map<*, *>, Exception>
|
||||
if (server == FileServerAPI.shared.server) {
|
||||
request.addHeader("Authorization", "Bearer loki")
|
||||
// Uploads to the Loki File Server shouldn't include any personally identifiable information, so use a dummy auth token
|
||||
promise = OnionRequestAPI.sendOnionRequest(request.build(), FileServerAPI.shared.server, FileServerAPI.fileServerPublicKey)
|
||||
} else {
|
||||
promise = FileServerAPI.shared.getPublicKeyForOpenGroupServer(server).bind { openGroupServerPublicKey ->
|
||||
getAuthToken(server).bind { token ->
|
||||
request.addHeader("Authorization", "Bearer $token")
|
||||
OnionRequestAPI.sendOnionRequest(request.build(), server, openGroupServerPublicKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
return promise.map { json ->
|
||||
parse(json)
|
||||
}.recover { exception ->
|
||||
if (exception is HTTP.HTTPRequestFailedException) {
|
||||
val statusCode = exception.statusCode
|
||||
if (statusCode == 401 || statusCode == 403) {
|
||||
MessagingModuleConfiguration.shared.storage.setAuthToken(server, null)
|
||||
}
|
||||
throw NonSuccessfulResponseCodeException("Request returned with status code ${exception.statusCode}.")
|
||||
}
|
||||
throw PushNetworkException(exception)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Boolean.toInt(): Int { return if (this) 1 else 0 }
|
@@ -6,7 +6,7 @@ import nl.komponents.kovenant.deferred
|
||||
import nl.komponents.kovenant.functional.bind
|
||||
import nl.komponents.kovenant.functional.map
|
||||
import okhttp3.Request
|
||||
import org.session.libsession.messaging.file_server.FileServerAPI
|
||||
import org.session.libsession.messaging.file_server.FileServerAPIV2
|
||||
import org.session.libsession.utilities.AESGCM
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.session.libsignal.utilities.Base64
|
||||
@@ -307,7 +307,7 @@ object OnionRequestAPI {
|
||||
val url = "${guardSnode.address}:${guardSnode.port}/onion_req/v2"
|
||||
val finalEncryptionResult = result.finalEncryptionResult
|
||||
val onion = finalEncryptionResult.ciphertext
|
||||
if (destination is Destination.Server && onion.count().toDouble() > 0.75 * FileServerAPI.maxFileSize.toDouble()) {
|
||||
if (destination is Destination.Server && onion.count().toDouble() > 0.75 * FileServerAPIV2.maxFileSize.toDouble()) {
|
||||
Log.d("Loki", "Approaching request size limit: ~${onion.count()} bytes.")
|
||||
}
|
||||
@Suppress("NAME_SHADOWING") val parameters = mapOf(
|
||||
|
@@ -1,15 +1,9 @@
|
||||
package org.session.libsession.utilities
|
||||
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.Request
|
||||
import org.session.libsession.messaging.file_server.FileServerAPI
|
||||
import org.session.libsession.messaging.file_server.FileServerAPIV2
|
||||
import org.session.libsession.snode.OnionRequestAPI
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.session.libsignal.messages.SignalServiceAttachment
|
||||
import org.session.libsignal.exceptions.NonSuccessfulResponseCodeException
|
||||
import org.session.libsignal.exceptions.PushNetworkException
|
||||
import org.session.libsignal.utilities.Base64
|
||||
import java.io.*
|
||||
|
||||
object DownloadUtilities {
|
||||
@@ -18,14 +12,14 @@ object DownloadUtilities {
|
||||
* Blocks the calling thread.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun downloadFile(destination: File, url: String, maxSize: Int, listener: SignalServiceAttachment.ProgressListener?) {
|
||||
fun downloadFile(destination: File, url: String) {
|
||||
val outputStream = FileOutputStream(destination) // Throws
|
||||
var remainingAttempts = 4
|
||||
var exception: Exception? = null
|
||||
while (remainingAttempts > 0) {
|
||||
remainingAttempts -= 1
|
||||
try {
|
||||
downloadFile(outputStream, url, maxSize, listener)
|
||||
downloadFile(outputStream, url)
|
||||
exception = null
|
||||
break
|
||||
} catch (e: Exception) {
|
||||
@@ -39,66 +33,16 @@ object DownloadUtilities {
|
||||
* Blocks the calling thread.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun downloadFile(outputStream: OutputStream, url: String, maxSize: Int, listener: SignalServiceAttachment.ProgressListener?) {
|
||||
|
||||
if (url.contains(FileServerAPIV2.SERVER) || url.contains(FileServerAPIV2.OLD_SERVER)) {
|
||||
val httpUrl = HttpUrl.parse(url)!!
|
||||
val fileId = httpUrl.pathSegments().last()
|
||||
val useOldServer = url.contains(FileServerAPIV2.OLD_SERVER)
|
||||
try {
|
||||
FileServerAPIV2.download(fileId.toLong(), useOldServer).get().let {
|
||||
outputStream.write(it)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("Loki", "Couln't download attachment due to error",e)
|
||||
throw e
|
||||
}
|
||||
} else {
|
||||
// We need to throw a PushNetworkException or NonSuccessfulResponseCodeException
|
||||
// because the underlying Signal logic requires these to work correctly
|
||||
val oldPrefixedHost = "https://" + HttpUrl.get(url).host()
|
||||
var newPrefixedHost = oldPrefixedHost
|
||||
if (oldPrefixedHost.contains(FileServerAPI.fileStorageBucketURL)) {
|
||||
newPrefixedHost = FileServerAPI.shared.server
|
||||
}
|
||||
// Edge case that needs to work: https://file-static.lokinet.org/i1pNmpInq3w9gF3TP8TFCa1rSo38J6UM
|
||||
// → https://file.getsession.org/loki/v1/f/XLxogNXVEIWHk14NVCDeppzTujPHxu35
|
||||
val fileID = url.substringAfter(oldPrefixedHost).substringAfter("/f/")
|
||||
val sanitizedURL = "$newPrefixedHost/loki/v1/f/$fileID"
|
||||
val request = Request.Builder().url(sanitizedURL).get()
|
||||
try {
|
||||
val serverPublicKey = if (newPrefixedHost.contains(FileServerAPI.shared.server)) FileServerAPI.fileServerPublicKey
|
||||
else FileServerAPI.shared.getPublicKeyForOpenGroupServer(newPrefixedHost).get()
|
||||
val json = OnionRequestAPI.sendOnionRequest(request.build(), newPrefixedHost, serverPublicKey, isJSONRequired = false).get()
|
||||
val result = json["result"] as? String
|
||||
if (result == null) {
|
||||
Log.d("Loki", "Couldn't parse attachment from: $json.")
|
||||
throw PushNetworkException("Missing response body.")
|
||||
}
|
||||
val body = Base64.decode(result)
|
||||
if (body.size > maxSize) {
|
||||
Log.d("Loki", "Attachment size limit exceeded.")
|
||||
throw PushNetworkException("Max response size exceeded.")
|
||||
}
|
||||
body.inputStream().use { input ->
|
||||
val buffer = ByteArray(32768)
|
||||
var count = 0
|
||||
var bytes = input.read(buffer)
|
||||
while (bytes >= 0) {
|
||||
outputStream.write(buffer, 0, bytes)
|
||||
count += bytes
|
||||
if (count > maxSize) {
|
||||
Log.d("Loki", "Attachment size limit exceeded.")
|
||||
throw PushNetworkException("Max response size exceeded.")
|
||||
}
|
||||
listener?.onAttachmentProgress(body.size.toLong(), count.toLong())
|
||||
bytes = input.read(buffer)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("Loki", "Couldn't download attachment due to error", e)
|
||||
throw if (e is NonSuccessfulResponseCodeException) e else PushNetworkException(e)
|
||||
fun downloadFile(outputStream: OutputStream, urlAsString: String) {
|
||||
val url = HttpUrl.parse(urlAsString)!!
|
||||
val fileID = url.pathSegments().last()
|
||||
try {
|
||||
FileServerAPIV2.download(fileID.toLong()).get().let {
|
||||
outputStream.write(it)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("Loki", "Couldn't download attachment.", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
@@ -36,7 +36,7 @@ object ProfilePictureUtilities {
|
||||
deferred.reject(e)
|
||||
}
|
||||
TextSecurePreferences.setLastProfilePictureUpload(context, Date().time)
|
||||
val url = "${FileServerAPIV2.SERVER}/files/$id"
|
||||
val url = "${FileServerAPIV2.server}/files/$id"
|
||||
TextSecurePreferences.setProfilePictureURL(context, url)
|
||||
deferred.resolve(Unit)
|
||||
}
|
||||
|
@@ -33,7 +33,6 @@ class SSKEnvironment(
|
||||
fun setProfilePictureURL(context: Context, recipient: Recipient, profilePictureURL: String)
|
||||
fun setProfileKey(context: Context, recipient: Recipient, profileKey: ByteArray)
|
||||
fun setUnidentifiedAccessMode(context: Context, recipient: Recipient, unidentifiedAccessMode: Recipient.UnidentifiedAccessMode)
|
||||
fun updateOpenGroupProfilePicturesIfNeeded(context: Context)
|
||||
}
|
||||
|
||||
interface MessageExpirationManagerProtocol {
|
||||
|
@@ -0,0 +1,3 @@
|
||||
package org.session.libsession.utilities
|
||||
|
||||
data class UploadResult(val id: Long, val url: String, val digest: ByteArray?)
|
Reference in New Issue
Block a user