mirror of
https://github.com/oxen-io/session-android.git
synced 2025-07-02 03:48:28 +00:00
Merge remote-tracking branch 'upstream/dev' into background_job_improvements
# Conflicts: # libsession/src/main/java/org/session/libsession/messaging/jobs/Job.kt # libsession/src/main/java/org/session/libsession/messaging/jobs/JobQueue.kt # libsession/src/main/java/org/session/libsession/messaging/jobs/MessageSendJob.kt
This commit is contained in:
commit
84fa3dfc86
@ -158,8 +158,8 @@ dependencies {
|
|||||||
testImplementation 'org.robolectric:shadows-multidex:4.2'
|
testImplementation 'org.robolectric:shadows-multidex:4.2'
|
||||||
}
|
}
|
||||||
|
|
||||||
def canonicalVersionCode = 158
|
def canonicalVersionCode = 159
|
||||||
def canonicalVersionName = "1.10.1"
|
def canonicalVersionName = "1.10.2"
|
||||||
|
|
||||||
def postFixSize = 10
|
def postFixSize = 10
|
||||||
def abiPostFix = ['armeabi-v7a' : 1,
|
def abiPostFix = ['armeabi-v7a' : 1,
|
||||||
|
@ -257,7 +257,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
val database = databaseHelper.readableDatabase
|
val database = databaseHelper.readableDatabase
|
||||||
return database.get(LokiThreadDatabase.publicChatTable, "${LokiThreadDatabase.threadID} = ?", arrayOf(threadId)) { cursor ->
|
return database.get(LokiThreadDatabase.publicChatTable, "${LokiThreadDatabase.threadID} = ?", arrayOf(threadId)) { cursor ->
|
||||||
val publicChatAsJson = cursor.getString(LokiThreadDatabase.publicChat)
|
val publicChatAsJson = cursor.getString(LokiThreadDatabase.publicChat)
|
||||||
OpenGroupV2.fromJson(publicChatAsJson)
|
OpenGroupV2.fromJSON(publicChatAsJson)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -195,7 +195,7 @@ class EnterChatURLFragment : Fragment() {
|
|||||||
chip.chipIcon = drawable
|
chip.chipIcon = drawable
|
||||||
chip.text = defaultGroup.name
|
chip.text = defaultGroup.name
|
||||||
chip.setOnClickListener {
|
chip.setOnClickListener {
|
||||||
(requireActivity() as JoinPublicChatActivity).joinPublicChatIfPossible(defaultGroup.toJoinUrl())
|
(requireActivity() as JoinPublicChatActivity).joinPublicChatIfPossible(defaultGroup.joinURL)
|
||||||
}
|
}
|
||||||
defaultRoomsGridLayout.addView(chip)
|
defaultRoomsGridLayout.addView(chip)
|
||||||
}
|
}
|
||||||
|
@ -58,6 +58,7 @@ class BackgroundPollWorker(val context: Context, params: WorkerParameters) : Wor
|
|||||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
|
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
|
||||||
val privateChatsPromise = SnodeAPI.getMessages(userPublicKey).map { envelopes ->
|
val privateChatsPromise = SnodeAPI.getMessages(userPublicKey).map { envelopes ->
|
||||||
envelopes.map { envelope ->
|
envelopes.map { envelope ->
|
||||||
|
// FIXME: Using a job here seems like a bad idea...
|
||||||
MessageReceiveJob(envelope.toByteArray(), false).executeAsync()
|
MessageReceiveJob(envelope.toByteArray(), false).executeAsync()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -68,7 +68,7 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
|
|||||||
while (cursor != null && cursor.moveToNext()) {
|
while (cursor != null && cursor.moveToNext()) {
|
||||||
val threadID = cursor.getLong(threadID)
|
val threadID = cursor.getLong(threadID)
|
||||||
val string = cursor.getString(publicChat)
|
val string = cursor.getString(publicChat)
|
||||||
val openGroup = OpenGroupV2.fromJson(string)
|
val openGroup = OpenGroupV2.fromJSON(string)
|
||||||
if (openGroup != null) result[threadID] = openGroup
|
if (openGroup != null) result[threadID] = openGroup
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@ -100,7 +100,7 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
|
|||||||
val database = databaseHelper.readableDatabase
|
val database = databaseHelper.readableDatabase
|
||||||
return database.get(publicChatTable, "${Companion.threadID} = ?", arrayOf(threadID.toString())) { cursor ->
|
return database.get(publicChatTable, "${Companion.threadID} = ?", arrayOf(threadID.toString())) { cursor ->
|
||||||
val json = cursor.getString(publicChat)
|
val json = cursor.getString(publicChat)
|
||||||
OpenGroupV2.fromJson(json)
|
OpenGroupV2.fromJSON(json)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,7 +18,8 @@ class SessionJobDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
|
|||||||
const val jobType = "job_type"
|
const val jobType = "job_type"
|
||||||
const val failureCount = "failure_count"
|
const val failureCount = "failure_count"
|
||||||
const val serializedData = "serialized_data"
|
const val serializedData = "serialized_data"
|
||||||
@JvmStatic val createSessionJobTableCommand = "CREATE TABLE $sessionJobTable ($jobID INTEGER PRIMARY KEY, $jobType STRING, $failureCount INTEGER DEFAULT 0, $serializedData TEXT);"
|
@JvmStatic val createSessionJobTableCommand
|
||||||
|
= "CREATE TABLE $sessionJobTable ($jobID INTEGER PRIMARY KEY, $jobType STRING, $failureCount INTEGER DEFAULT 0, $serializedData TEXT);"
|
||||||
}
|
}
|
||||||
|
|
||||||
fun persistJob(job: Job) {
|
fun persistJob(job: Job) {
|
||||||
@ -31,40 +32,41 @@ class SessionJobDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
|
|||||||
database.insertOrUpdate(sessionJobTable, contentValues, "$jobID = ?", arrayOf(jobID))
|
database.insertOrUpdate(sessionJobTable, contentValues, "$jobID = ?", arrayOf(jobID))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun markJobAsSucceeded(jobId: String) {
|
fun markJobAsSucceeded(jobID: String) {
|
||||||
databaseHelper.writableDatabase.delete(sessionJobTable, "$jobID = ?", arrayOf(jobId))
|
databaseHelper.writableDatabase.delete(sessionJobTable, "$jobID = ?", arrayOf( jobID ))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun markJobAsFailed(jobId: String) {
|
fun markJobAsFailed(jobID: String) {
|
||||||
databaseHelper.writableDatabase.delete(sessionJobTable, "$jobID = ?", arrayOf(jobId))
|
databaseHelper.writableDatabase.delete(sessionJobTable, "$jobID = ?", arrayOf( jobID ))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAllPendingJobs(type: String): Map<String, Job?> {
|
fun getAllPendingJobs(type: String): Map<String, Job?> {
|
||||||
val database = databaseHelper.readableDatabase
|
val database = databaseHelper.readableDatabase
|
||||||
return database.getAll(sessionJobTable, "$jobType = ?", arrayOf(type)) { cursor ->
|
return database.getAll(sessionJobTable, "$jobType = ?", arrayOf( type )) { cursor ->
|
||||||
val jobId = cursor.getString(jobID)
|
val jobID = cursor.getString(jobID)
|
||||||
try {
|
try {
|
||||||
jobId to jobFromCursor(cursor)
|
jobID to jobFromCursor(cursor)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("Loki", "Error serializing Job of type $type",e)
|
Log.e("Loki", "Error deserializing job of type: $type.", e)
|
||||||
jobId to null
|
jobID to null
|
||||||
}
|
}
|
||||||
}.toMap()
|
}.toMap()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAttachmentUploadJob(attachmentID: Long): AttachmentUploadJob? {
|
fun getAttachmentUploadJob(attachmentID: Long): AttachmentUploadJob? {
|
||||||
val database = databaseHelper.readableDatabase
|
val database = databaseHelper.readableDatabase
|
||||||
var result = mutableListOf<AttachmentUploadJob>()
|
val result = mutableListOf<AttachmentUploadJob>()
|
||||||
database.getAll(sessionJobTable, "$jobType = ?", arrayOf(AttachmentUploadJob.KEY)) { cursor ->
|
database.getAll(sessionJobTable, "$jobType = ?", arrayOf( AttachmentUploadJob.KEY )) { cursor ->
|
||||||
result.add(jobFromCursor(cursor) as AttachmentUploadJob)
|
val job = jobFromCursor(cursor) as AttachmentUploadJob?
|
||||||
|
if (job != null) { result.add(job) }
|
||||||
}
|
}
|
||||||
return result.firstOrNull { job -> job.attachmentID == attachmentID }
|
return result.firstOrNull { job -> job.attachmentID == attachmentID }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getMessageSendJob(messageSendJobID: String): MessageSendJob? {
|
fun getMessageSendJob(messageSendJobID: String): MessageSendJob? {
|
||||||
val database = databaseHelper.readableDatabase
|
val database = databaseHelper.readableDatabase
|
||||||
return database.get(sessionJobTable, "$jobID = ? AND $jobType = ?", arrayOf(messageSendJobID, MessageSendJob.KEY)) { cursor ->
|
return database.get(sessionJobTable, "$jobID = ? AND $jobType = ?", arrayOf( messageSendJobID, MessageSendJob.KEY )) { cursor ->
|
||||||
jobFromCursor(cursor) as MessageSendJob
|
jobFromCursor(cursor) as MessageSendJob?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,7 +74,7 @@ class SessionJobDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
|
|||||||
val database = databaseHelper.readableDatabase
|
val database = databaseHelper.readableDatabase
|
||||||
var cursor: android.database.Cursor? = null
|
var cursor: android.database.Cursor? = null
|
||||||
try {
|
try {
|
||||||
cursor = database.rawQuery("SELECT * FROM $sessionJobTable WHERE $jobID = ?", arrayOf(job.id))
|
cursor = database.rawQuery("SELECT * FROM $sessionJobTable WHERE $jobID = ?", arrayOf( job.id ))
|
||||||
return cursor != null && cursor.moveToFirst()
|
return cursor != null && cursor.moveToFirst()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// Do nothing
|
// Do nothing
|
||||||
@ -82,10 +84,10 @@ class SessionJobDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun jobFromCursor(cursor: Cursor): Job {
|
private fun jobFromCursor(cursor: Cursor): Job? {
|
||||||
val type = cursor.getString(jobType)
|
val type = cursor.getString(jobType)
|
||||||
val data = SessionJobHelper.dataSerializer.deserialize(cursor.getString(serializedData))
|
val data = SessionJobHelper.dataSerializer.deserialize(cursor.getString(serializedData))
|
||||||
val job = SessionJobHelper.sessionJobInstantiator.instantiate(type, data)
|
val job = SessionJobHelper.sessionJobInstantiator.instantiate(type, data) ?: return null
|
||||||
job.id = cursor.getString(jobID)
|
job.id = cursor.getString(jobID)
|
||||||
job.failureCount = cursor.getInt(failureCount)
|
job.failureCount = cursor.getInt(failureCount)
|
||||||
return job
|
return job
|
||||||
|
@ -8,8 +8,8 @@ class MessagingModuleConfiguration(
|
|||||||
val context: Context,
|
val context: Context,
|
||||||
val storage: StorageProtocol,
|
val storage: StorageProtocol,
|
||||||
val messageDataProvider: MessageDataProvider,
|
val messageDataProvider: MessageDataProvider,
|
||||||
val sessionProtocol: SessionProtocol)
|
val sessionProtocol: SessionProtocol
|
||||||
{
|
) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
lateinit var shared: MessagingModuleConfiguration
|
lateinit var shared: MessagingModuleConfiguration
|
||||||
|
@ -1,106 +1,91 @@
|
|||||||
package org.session.libsession.messaging.file_server
|
package org.session.libsession.messaging.file_server
|
||||||
|
|
||||||
import nl.komponents.kovenant.Promise
|
import nl.komponents.kovenant.Promise
|
||||||
import nl.komponents.kovenant.functional.bind
|
|
||||||
import nl.komponents.kovenant.functional.map
|
import nl.komponents.kovenant.functional.map
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
import okhttp3.MediaType
|
import okhttp3.MediaType
|
||||||
import okhttp3.RequestBody
|
import okhttp3.RequestBody
|
||||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
|
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
|
||||||
import org.session.libsession.snode.OnionRequestAPI
|
import org.session.libsession.snode.OnionRequestAPI
|
||||||
import org.session.libsignal.service.loki.HTTP
|
import org.session.libsignal.service.loki.HTTP
|
||||||
import org.session.libsignal.service.loki.utilities.retryIfNeeded
|
|
||||||
import org.session.libsignal.utilities.Base64
|
import org.session.libsignal.utilities.Base64
|
||||||
import org.session.libsignal.utilities.JsonUtil
|
import org.session.libsignal.utilities.JsonUtil
|
||||||
import org.session.libsignal.utilities.logging.Log
|
import org.session.libsignal.utilities.logging.Log
|
||||||
|
|
||||||
object FileServerAPIV2 {
|
object FileServerAPIV2 {
|
||||||
|
|
||||||
const val DEFAULT_SERVER = "http://88.99.175.227"
|
|
||||||
private const val DEFAULT_SERVER_PUBLIC_KEY = "7cb31905b55cd5580c686911debf672577b3fb0bff81df4ce2d5c4cb3a7aaa69"
|
private const val DEFAULT_SERVER_PUBLIC_KEY = "7cb31905b55cd5580c686911debf672577b3fb0bff81df4ce2d5c4cb3a7aaa69"
|
||||||
|
const val DEFAULT_SERVER = "http://88.99.175.227"
|
||||||
|
|
||||||
sealed class Error : Exception() {
|
sealed class Error(message: String) : Exception(message) {
|
||||||
object PARSING_FAILED : Error()
|
object ParsingFailed : Error("Invalid response.")
|
||||||
object INVALID_URL : Error()
|
object InvalidURL : Error("Invalid URL.")
|
||||||
|
|
||||||
fun errorDescription() = when (this) {
|
|
||||||
PARSING_FAILED -> "Invalid response."
|
|
||||||
INVALID_URL -> "Invalid URL."
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Request(
|
data class Request(
|
||||||
val verb: HTTP.Verb,
|
val verb: HTTP.Verb,
|
||||||
val endpoint: String,
|
val endpoint: String,
|
||||||
val queryParameters: Map<String, String> = mapOf(),
|
val queryParameters: Map<String, String> = mapOf(),
|
||||||
val parameters: Any? = null,
|
val parameters: Any? = null,
|
||||||
val headers: Map<String, String> = mapOf(),
|
val headers: Map<String, String> = mapOf(),
|
||||||
// Always `true` under normal circumstances. You might want to disable
|
/**
|
||||||
// this when running over Lokinet.
|
* Always `true` under normal circumstances. You might want to disable
|
||||||
val useOnionRouting: Boolean = true
|
* this when running over Lokinet.
|
||||||
|
*/
|
||||||
|
val useOnionRouting: Boolean = true
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun createBody(parameters: Any?): RequestBody? {
|
private fun createBody(parameters: Any?): RequestBody? {
|
||||||
if (parameters == null) return null
|
if (parameters == null) return null
|
||||||
|
|
||||||
val parametersAsJSON = JsonUtil.toJson(parameters)
|
val parametersAsJSON = JsonUtil.toJson(parameters)
|
||||||
return RequestBody.create(MediaType.get("application/json"), parametersAsJSON)
|
return RequestBody.create(MediaType.get("application/json"), parametersAsJSON)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun send(request: Request): Promise<Map<*, *>, Exception> {
|
private fun send(request: Request): Promise<Map<*, *>, Exception> {
|
||||||
val parsed = HttpUrl.parse(DEFAULT_SERVER) ?: return Promise.ofFail(OpenGroupAPIV2.Error.INVALID_URL)
|
val url = HttpUrl.parse(DEFAULT_SERVER) ?: return Promise.ofFail(OpenGroupAPIV2.Error.InvalidURL)
|
||||||
val urlBuilder = HttpUrl.Builder()
|
val urlBuilder = HttpUrl.Builder()
|
||||||
.scheme(parsed.scheme())
|
.scheme(url.scheme())
|
||||||
.host(parsed.host())
|
.host(url.host())
|
||||||
.port(parsed.port())
|
.port(url.port())
|
||||||
.addPathSegments(request.endpoint)
|
.addPathSegments(request.endpoint)
|
||||||
|
|
||||||
if (request.verb == HTTP.Verb.GET) {
|
if (request.verb == HTTP.Verb.GET) {
|
||||||
for ((key, value) in request.queryParameters) {
|
for ((key, value) in request.queryParameters) {
|
||||||
urlBuilder.addQueryParameter(key, value)
|
urlBuilder.addQueryParameter(key, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val requestBuilder = okhttp3.Request.Builder()
|
val requestBuilder = okhttp3.Request.Builder()
|
||||||
.url(urlBuilder.build())
|
.url(urlBuilder.build())
|
||||||
.headers(Headers.of(request.headers))
|
.headers(Headers.of(request.headers))
|
||||||
when (request.verb) {
|
when (request.verb) {
|
||||||
HTTP.Verb.GET -> requestBuilder.get()
|
HTTP.Verb.GET -> requestBuilder.get()
|
||||||
HTTP.Verb.PUT -> requestBuilder.put(createBody(request.parameters)!!)
|
HTTP.Verb.PUT -> requestBuilder.put(createBody(request.parameters)!!)
|
||||||
HTTP.Verb.POST -> requestBuilder.post(createBody(request.parameters)!!)
|
HTTP.Verb.POST -> requestBuilder.post(createBody(request.parameters)!!)
|
||||||
HTTP.Verb.DELETE -> requestBuilder.delete(createBody(request.parameters))
|
HTTP.Verb.DELETE -> requestBuilder.delete(createBody(request.parameters))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.useOnionRouting) {
|
if (request.useOnionRouting) {
|
||||||
return OnionRequestAPI.sendOnionRequest(requestBuilder.build(), DEFAULT_SERVER, DEFAULT_SERVER_PUBLIC_KEY)
|
return OnionRequestAPI.sendOnionRequest(requestBuilder.build(), DEFAULT_SERVER, DEFAULT_SERVER_PUBLIC_KEY).fail { e ->
|
||||||
.fail { e ->
|
Log.e("Loki", "File server request failed.", e)
|
||||||
Log.e("Loki", "FileServerV2 failed with error",e)
|
}
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
return Promise.ofFail(IllegalStateException("It's currently not allowed to send non onion routed requests."))
|
return Promise.ofFail(IllegalStateException("It's currently not allowed to send non onion routed requests."))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// region Sending
|
|
||||||
fun upload(file: ByteArray): Promise<Long, Exception> {
|
fun upload(file: ByteArray): Promise<Long, Exception> {
|
||||||
val base64EncodedFile = Base64.encodeBytes(file)
|
val base64EncodedFile = Base64.encodeBytes(file)
|
||||||
val parameters = mapOf("file" to base64EncodedFile)
|
val parameters = mapOf( "file" to base64EncodedFile )
|
||||||
val request = Request(verb = HTTP.Verb.POST, endpoint = "files", parameters = parameters)
|
val request = Request(verb = HTTP.Verb.POST, endpoint = "files", parameters = parameters)
|
||||||
return send(request).map { json ->
|
return send(request).map { json ->
|
||||||
json["result"] as? Long ?: throw OpenGroupAPIV2.Error.PARSING_FAILED
|
json["result"] as? Long ?: throw OpenGroupAPIV2.Error.ParsingFailed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun download(file: Long): Promise<ByteArray, Exception> {
|
fun download(file: Long): Promise<ByteArray, Exception> {
|
||||||
val request = Request(verb = HTTP.Verb.GET, endpoint = "files/$file")
|
val request = Request(verb = HTTP.Verb.GET, endpoint = "files/$file")
|
||||||
return send(request).map { json ->
|
return send(request).map { json ->
|
||||||
val base64EncodedFile = json["result"] as? String ?: throw Error.PARSING_FAILED
|
val base64EncodedFile = json["result"] as? String ?: throw Error.ParsingFailed
|
||||||
Base64.decode(base64EncodedFile) ?: throw Error.PARSING_FAILED
|
Base64.decode(base64EncodedFile) ?: throw Error.ParsingFailed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -3,7 +3,6 @@ package org.session.libsession.messaging.jobs
|
|||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||||
import org.session.libsession.messaging.file_server.FileServerAPI
|
import org.session.libsession.messaging.file_server.FileServerAPI
|
||||||
import org.session.libsession.messaging.file_server.FileServerAPIV2
|
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
|
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentState
|
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentState
|
||||||
import org.session.libsession.messaging.utilities.DotNetAPI
|
import org.session.libsession.messaging.utilities.DotNetAPI
|
||||||
@ -31,8 +30,8 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long)
|
|||||||
val KEY: String = "AttachmentDownloadJob"
|
val KEY: String = "AttachmentDownloadJob"
|
||||||
|
|
||||||
// Keys used for database storage
|
// Keys used for database storage
|
||||||
private val KEY_ATTACHMENT_ID = "attachment_id"
|
private val ATTACHMENT_ID_KEY = "attachment_id"
|
||||||
private val KEY_TS_INCOMING_MESSAGE_ID = "tsIncoming_message_id"
|
private val TS_INCOMING_MESSAGE_ID_KEY = "tsIncoming_message_id"
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun execute() {
|
override fun execute() {
|
||||||
@ -52,18 +51,19 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long)
|
|||||||
try {
|
try {
|
||||||
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
|
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
|
||||||
val attachment = messageDataProvider.getDatabaseAttachment(attachmentID)
|
val attachment = messageDataProvider.getDatabaseAttachment(attachmentID)
|
||||||
?: return handleFailure(Error.NoAttachment)
|
?: return handleFailure(Error.NoAttachment)
|
||||||
messageDataProvider.setAttachmentState(AttachmentState.STARTED, attachmentID, this.databaseMessageID)
|
messageDataProvider.setAttachmentState(AttachmentState.STARTED, attachmentID, this.databaseMessageID)
|
||||||
val tempFile = createTempFile()
|
val tempFile = createTempFile()
|
||||||
|
|
||||||
val threadId = MessagingModuleConfiguration.shared.storage.getThreadIdForMms(databaseMessageID)
|
val threadId = MessagingModuleConfiguration.shared.storage.getThreadIdForMms(databaseMessageID)
|
||||||
val openGroupV2 = MessagingModuleConfiguration.shared.storage.getV2OpenGroup(threadId.toString())
|
val openGroupV2 = MessagingModuleConfiguration.shared.storage.getV2OpenGroup(threadId.toString())
|
||||||
|
|
||||||
val stream = if (openGroupV2 == null) {
|
val stream = if (openGroupV2 == null) {
|
||||||
DownloadUtilities.downloadFile(tempFile, attachment.url, FileServerAPI.maxFileSize, null)
|
DownloadUtilities.downloadFile(tempFile, attachment.url, FileServerAPI.maxFileSize, null)
|
||||||
// Assume we're retrieving an attachment for an open group server if the digest is not set
|
// 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)
|
if (attachment.digest?.size ?: 0 == 0 || attachment.key.isNullOrEmpty()) {
|
||||||
else AttachmentCipherInputStream.createForAttachment(tempFile, attachment.size, Base64.decode(attachment.key), attachment.digest)
|
FileInputStream(tempFile)
|
||||||
|
} else {
|
||||||
|
AttachmentCipherInputStream.createForAttachment(tempFile, attachment.size, Base64.decode(attachment.key), attachment.digest)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
val url = HttpUrl.parse(attachment.url)!!
|
val url = HttpUrl.parse(attachment.url)!!
|
||||||
val fileId = url.pathSegments().last()
|
val fileId = url.pathSegments().last()
|
||||||
@ -100,8 +100,9 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long)
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun serialize(): Data {
|
override fun serialize(): Data {
|
||||||
return Data.Builder().putLong(KEY_ATTACHMENT_ID, attachmentID)
|
return Data.Builder()
|
||||||
.putLong(KEY_TS_INCOMING_MESSAGE_ID, databaseMessageID)
|
.putLong(ATTACHMENT_ID_KEY, attachmentID)
|
||||||
|
.putLong(TS_INCOMING_MESSAGE_ID_KEY, databaseMessageID)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,8 +111,9 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long)
|
|||||||
}
|
}
|
||||||
|
|
||||||
class Factory : Job.Factory<AttachmentDownloadJob> {
|
class Factory : Job.Factory<AttachmentDownloadJob> {
|
||||||
|
|
||||||
override fun create(data: Data): AttachmentDownloadJob {
|
override fun create(data: Data): AttachmentDownloadJob {
|
||||||
return AttachmentDownloadJob(data.getLong(KEY_ATTACHMENT_ID), data.getLong(KEY_TS_INCOMING_MESSAGE_ID))
|
return AttachmentDownloadJob(data.getLong(ATTACHMENT_ID_KEY), data.getLong(TS_INCOMING_MESSAGE_ID_KEY))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -30,44 +30,39 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
|
|||||||
|
|
||||||
// Settings
|
// Settings
|
||||||
override val maxFailureCount: Int = 20
|
override val maxFailureCount: Int = 20
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val TAG = AttachmentUploadJob::class.simpleName
|
val TAG = AttachmentUploadJob::class.simpleName
|
||||||
val KEY: String = "AttachmentUploadJob"
|
val KEY: String = "AttachmentUploadJob"
|
||||||
|
|
||||||
// Keys used for database storage
|
// Keys used for database storage
|
||||||
private val KEY_ATTACHMENT_ID = "attachment_id"
|
private val ATTACHMENT_ID_KEY = "attachment_id"
|
||||||
private val KEY_THREAD_ID = "thread_id"
|
private val THREAD_ID_KEY = "thread_id"
|
||||||
private val KEY_MESSAGE = "message"
|
private val MESSAGE_KEY = "message"
|
||||||
private val KEY_MESSAGE_SEND_JOB_ID = "message_send_job_id"
|
private val MESSAGE_SEND_JOB_ID_KEY = "message_send_job_id"
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun execute() {
|
override fun execute() {
|
||||||
try {
|
try {
|
||||||
val attachment = MessagingModuleConfiguration.shared.messageDataProvider.getScaledSignalAttachmentStream(attachmentID)
|
val attachment = MessagingModuleConfiguration.shared.messageDataProvider.getScaledSignalAttachmentStream(attachmentID)
|
||||||
?: return handleFailure(Error.NoAttachment)
|
?: return handleFailure(Error.NoAttachment)
|
||||||
|
|
||||||
val usePadding = false
|
val usePadding = false
|
||||||
val openGroupV2 = MessagingModuleConfiguration.shared.storage.getV2OpenGroup(threadID)
|
val openGroupV2 = MessagingModuleConfiguration.shared.storage.getV2OpenGroup(threadID)
|
||||||
val openGroup = MessagingModuleConfiguration.shared.storage.getOpenGroup(threadID)
|
val openGroup = MessagingModuleConfiguration.shared.storage.getOpenGroup(threadID)
|
||||||
val server = openGroup?.let {
|
val server = openGroupV2?.server ?: openGroup?.server ?: FileServerAPI.shared.server
|
||||||
it.server
|
|
||||||
} ?: openGroupV2?.let {
|
|
||||||
it.server
|
|
||||||
} ?: FileServerAPI.shared.server
|
|
||||||
val shouldEncrypt = (openGroup == null && openGroupV2 == null) // Encrypt if this isn't an open group
|
val shouldEncrypt = (openGroup == null && openGroupV2 == null) // Encrypt if this isn't an open group
|
||||||
|
|
||||||
val attachmentKey = Util.getSecretBytes(64)
|
val attachmentKey = Util.getSecretBytes(64)
|
||||||
val paddedLength = if (usePadding) PaddingInputStream.getPaddedSize(attachment.length) else attachment.length
|
val paddedLength = if (usePadding) PaddingInputStream.getPaddedSize(attachment.length) else attachment.length
|
||||||
val dataStream = if (usePadding) PaddingInputStream(attachment.inputStream, attachment.length) else attachment.inputStream
|
val dataStream = if (usePadding) PaddingInputStream(attachment.inputStream, attachment.length) else attachment.inputStream
|
||||||
val ciphertextLength = if (shouldEncrypt) AttachmentCipherOutputStream.getCiphertextLength(paddedLength) else attachment.length
|
val ciphertextLength = if (shouldEncrypt) AttachmentCipherOutputStream.getCiphertextLength(paddedLength) else attachment.length
|
||||||
|
|
||||||
val outputStreamFactory = if (shouldEncrypt) AttachmentCipherOutputStreamFactory(attachmentKey) else PlaintextOutputStreamFactory()
|
val outputStreamFactory = if (shouldEncrypt) AttachmentCipherOutputStreamFactory(attachmentKey) else PlaintextOutputStreamFactory()
|
||||||
val attachmentData = PushAttachmentData(attachment.contentType, dataStream, ciphertextLength, outputStreamFactory, attachment.listener)
|
val attachmentData = PushAttachmentData(attachment.contentType, dataStream, ciphertextLength, outputStreamFactory, attachment.listener)
|
||||||
|
val uploadResult = if (openGroupV2 != null) {
|
||||||
val uploadResult = if (openGroupV2 == null) FileServerAPI.shared.uploadAttachment(server, attachmentData) else {
|
|
||||||
val dataBytes = attachmentData.data.readBytes()
|
val dataBytes = attachmentData.data.readBytes()
|
||||||
val result = OpenGroupAPIV2.upload(dataBytes, openGroupV2.room, openGroupV2.server).get()
|
val result = OpenGroupAPIV2.upload(dataBytes, openGroupV2.room, openGroupV2.server).get()
|
||||||
DotNetAPI.UploadResult(result, "${openGroupV2.server}/files/$result", byteArrayOf())
|
DotNetAPI.UploadResult(result, "${openGroupV2.server}/files/$result", byteArrayOf())
|
||||||
|
} else {
|
||||||
|
FileServerAPI.shared.uploadAttachment(server, attachmentData)
|
||||||
}
|
}
|
||||||
handleSuccess(attachment, attachmentKey, uploadResult)
|
handleSuccess(attachment, attachmentKey, uploadResult)
|
||||||
} catch (e: java.lang.Exception) {
|
} catch (e: java.lang.Exception) {
|
||||||
@ -82,7 +77,7 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun handleSuccess(attachment: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: DotNetAPI.UploadResult) {
|
private fun handleSuccess(attachment: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: DotNetAPI.UploadResult) {
|
||||||
Log.w(TAG, "Attachment uploaded successfully.")
|
Log.d(TAG, "Attachment uploaded successfully.")
|
||||||
delegate?.handleJobSucceeded(this)
|
delegate?.handleJobSucceeded(this)
|
||||||
MessagingModuleConfiguration.shared.messageDataProvider.updateAttachmentAfterUploadSucceeded(attachmentID, attachment, attachmentKey, uploadResult)
|
MessagingModuleConfiguration.shared.messageDataProvider.updateAttachmentAfterUploadSucceeded(attachmentID, attachment, attachmentKey, uploadResult)
|
||||||
MessagingModuleConfiguration.shared.storage.resumeMessageSendJobIfNeeded(messageSendJobID)
|
MessagingModuleConfiguration.shared.storage.resumeMessageSendJobIfNeeded(messageSendJobID)
|
||||||
@ -119,10 +114,11 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
|
|||||||
val output = Output(serializedMessage)
|
val output = Output(serializedMessage)
|
||||||
kryo.writeObject(output, message)
|
kryo.writeObject(output, message)
|
||||||
output.close()
|
output.close()
|
||||||
return Data.Builder().putLong(KEY_ATTACHMENT_ID, attachmentID)
|
return Data.Builder()
|
||||||
.putString(KEY_THREAD_ID, threadID)
|
.putLong(ATTACHMENT_ID_KEY, attachmentID)
|
||||||
.putByteArray(KEY_MESSAGE, serializedMessage)
|
.putString(THREAD_ID_KEY, threadID)
|
||||||
.putString(KEY_MESSAGE_SEND_JOB_ID, messageSendJobID)
|
.putByteArray(MESSAGE_KEY, serializedMessage)
|
||||||
|
.putString(MESSAGE_SEND_JOB_ID_KEY, messageSendJobID)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -133,13 +129,18 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
|
|||||||
class Factory: Job.Factory<AttachmentUploadJob> {
|
class Factory: Job.Factory<AttachmentUploadJob> {
|
||||||
|
|
||||||
override fun create(data: Data): AttachmentUploadJob {
|
override fun create(data: Data): AttachmentUploadJob {
|
||||||
val serializedMessage = data.getByteArray(KEY_MESSAGE)
|
val serializedMessage = data.getByteArray(MESSAGE_KEY)
|
||||||
val kryo = Kryo()
|
val kryo = Kryo()
|
||||||
kryo.isRegistrationRequired = false
|
kryo.isRegistrationRequired = false
|
||||||
val input = Input(serializedMessage)
|
val input = Input(serializedMessage)
|
||||||
val message: Message = kryo.readObject(input, Message::class.java)
|
val message: Message = kryo.readObject(input, Message::class.java)
|
||||||
input.close()
|
input.close()
|
||||||
return AttachmentUploadJob(data.getLong(KEY_ATTACHMENT_ID), data.getString(KEY_THREAD_ID)!!, message, data.getString(KEY_MESSAGE_SEND_JOB_ID)!!)
|
return AttachmentUploadJob(
|
||||||
|
data.getLong(ATTACHMENT_ID_KEY),
|
||||||
|
data.getString(THREAD_ID_KEY)!!,
|
||||||
|
message,
|
||||||
|
data.getString(MESSAGE_SEND_JOB_ID_KEY)!!
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -8,9 +8,10 @@ interface Job {
|
|||||||
val maxFailureCount: Int
|
val maxFailureCount: Int
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
// Keys used for database storage
|
// Keys used for database storage
|
||||||
private val KEY_ID = "id"
|
private val ID_KEY = "id"
|
||||||
private val KEY_FAILURE_COUNT = "failure_count"
|
private val FAILURE_COUNT_KEY = "failure_count"
|
||||||
internal const val MAX_BUFFER_SIZE = 1_000_000 // bytes
|
internal const val MAX_BUFFER_SIZE = 1_000_000 // bytes
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -18,12 +19,10 @@ interface Job {
|
|||||||
|
|
||||||
fun serialize(): Data
|
fun serialize(): Data
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the key that can be used to find the relevant factory needed to create your job.
|
|
||||||
*/
|
|
||||||
fun getFactoryKey(): String
|
fun getFactoryKey(): String
|
||||||
|
|
||||||
interface Factory<T : Job> {
|
interface Factory<T : Job> {
|
||||||
fun create(data: Data): T
|
|
||||||
|
fun create(data: Data): T?
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -44,13 +44,14 @@ class JobQueue : JobDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
val shared: JobQueue by lazy { JobQueue() }
|
val shared: JobQueue by lazy { JobQueue() }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Job.canExecuteParallel(): Boolean {
|
private fun Job.canExecuteParallel(): Boolean {
|
||||||
return this.javaClass in arrayOf(
|
return this.javaClass in arrayOf(
|
||||||
MessageSendJob::class.java,
|
MessageSendJob::class.java,
|
||||||
AttachmentUploadJob::class.java,
|
AttachmentUploadJob::class.java,
|
||||||
AttachmentDownloadJob::class.java
|
AttachmentDownloadJob::class.java
|
||||||
)
|
)
|
||||||
@ -69,7 +70,6 @@ class JobQueue : JobDelegate {
|
|||||||
val currentTime = System.currentTimeMillis()
|
val currentTime = System.currentTimeMillis()
|
||||||
jobTimestampMap.putIfAbsent(currentTime, AtomicInteger())
|
jobTimestampMap.putIfAbsent(currentTime, AtomicInteger())
|
||||||
job.id = currentTime.toString() + jobTimestampMap[currentTime]!!.getAndIncrement().toString()
|
job.id = currentTime.toString() + jobTimestampMap[currentTime]!!.getAndIncrement().toString()
|
||||||
|
|
||||||
MessagingModuleConfiguration.shared.storage.persistJob(job)
|
MessagingModuleConfiguration.shared.storage.persistJob(job)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,25 +79,26 @@ class JobQueue : JobDelegate {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
hasResumedPendingJobs = true
|
hasResumedPendingJobs = true
|
||||||
val allJobTypes = listOf(AttachmentUploadJob.KEY,
|
val allJobTypes = listOf(
|
||||||
AttachmentDownloadJob.KEY,
|
AttachmentUploadJob.KEY,
|
||||||
MessageReceiveJob.KEY,
|
AttachmentDownloadJob.KEY,
|
||||||
MessageSendJob.KEY,
|
MessageReceiveJob.KEY,
|
||||||
NotifyPNServerJob.KEY
|
MessageSendJob.KEY,
|
||||||
|
NotifyPNServerJob.KEY
|
||||||
)
|
)
|
||||||
allJobTypes.forEach { type ->
|
allJobTypes.forEach { type ->
|
||||||
val allPendingJobs = MessagingModuleConfiguration.shared.storage.getAllPendingJobs(type)
|
val allPendingJobs = MessagingModuleConfiguration.shared.storage.getAllPendingJobs(type)
|
||||||
val pendingJobs = mutableListOf<Job>()
|
val pendingJobs = mutableListOf<Job>()
|
||||||
for ((id, job) in allPendingJobs) {
|
for ((id, job) in allPendingJobs) {
|
||||||
if (job == null) {
|
if (job == null) {
|
||||||
// job failed to serialize, remove it from the DB
|
// Job failed to deserialize, remove it from the DB
|
||||||
handleJobFailedPermanently(id)
|
handleJobFailedPermanently(id)
|
||||||
} else {
|
} else {
|
||||||
pendingJobs.add(job)
|
pendingJobs.add(job)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pendingJobs.sortedBy { it.id }.forEach { job ->
|
pendingJobs.sortedBy { it.id }.forEach { job ->
|
||||||
Log.i("Jobs", "Resuming pending job of type: ${job::class.simpleName}.")
|
Log.i("Loki", "Resuming pending job of type: ${job::class.simpleName}.")
|
||||||
queue.offer(job) // Offer always called on unlimited capacity
|
queue.offer(job) // Offer always called on unlimited capacity
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -111,15 +112,15 @@ class JobQueue : JobDelegate {
|
|||||||
override fun handleJobFailed(job: Job, error: Exception) {
|
override fun handleJobFailed(job: Job, error: Exception) {
|
||||||
job.failureCount += 1
|
job.failureCount += 1
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
if (storage.isJobCanceled(job)) { return Log.i("Jobs", "${job::class.simpleName} canceled.")}
|
if (storage.isJobCanceled(job)) { return Log.i("Loki", "${job::class.simpleName} canceled.")}
|
||||||
if (job.failureCount == job.maxFailureCount) {
|
if (job.failureCount == job.maxFailureCount) {
|
||||||
handleJobFailedPermanently(job, error)
|
handleJobFailedPermanently(job, error)
|
||||||
} else {
|
} else {
|
||||||
storage.persistJob(job)
|
storage.persistJob(job)
|
||||||
val retryInterval = getRetryInterval(job)
|
val retryInterval = getRetryInterval(job)
|
||||||
Log.i("Jobs", "${job::class.simpleName} failed; scheduling retry (failure count is ${job.failureCount}).")
|
Log.i("Loki", "${job::class.simpleName} failed; scheduling retry (failure count is ${job.failureCount}).")
|
||||||
timer.schedule(delay = retryInterval) {
|
timer.schedule(delay = retryInterval) {
|
||||||
Log.i("Jobs", "Retrying ${job::class.simpleName}.")
|
Log.i("Loki", "Retrying ${job::class.simpleName}.")
|
||||||
queue.offer(job)
|
queue.offer(job)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,6 @@ class MessageReceiveJob(val data: ByteArray, val isBackgroundPoll: Boolean, val
|
|||||||
override var id: String? = null
|
override var id: String? = null
|
||||||
override var failureCount: Int = 0
|
override var failureCount: Int = 0
|
||||||
|
|
||||||
// Settings
|
|
||||||
override val maxFailureCount: Int = 10
|
override val maxFailureCount: Int = 10
|
||||||
companion object {
|
companion object {
|
||||||
val TAG = MessageReceiveJob::class.simpleName
|
val TAG = MessageReceiveJob::class.simpleName
|
||||||
@ -20,10 +19,11 @@ class MessageReceiveJob(val data: ByteArray, val isBackgroundPoll: Boolean, val
|
|||||||
private val RECEIVE_LOCK = Object()
|
private val RECEIVE_LOCK = Object()
|
||||||
|
|
||||||
// Keys used for database storage
|
// Keys used for database storage
|
||||||
private val KEY_DATA = "data"
|
private val DATA_KEY = "data"
|
||||||
private val KEY_IS_BACKGROUND_POLL = "is_background_poll"
|
// FIXME: We probably shouldn't be using this job when background polling
|
||||||
private val KEY_OPEN_GROUP_MESSAGE_SERVER_ID = "openGroupMessageServerID"
|
private val IS_BACKGROUND_POLL_KEY = "is_background_poll"
|
||||||
private val KEY_OPEN_GROUP_ID = "open_group_id"
|
private val OPEN_GROUP_MESSAGE_SERVER_ID_KEY = "openGroupMessageServerID"
|
||||||
|
private val OPEN_GROUP_ID_KEY = "open_group_id"
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun execute() {
|
override fun execute() {
|
||||||
@ -35,19 +35,18 @@ class MessageReceiveJob(val data: ByteArray, val isBackgroundPoll: Boolean, val
|
|||||||
try {
|
try {
|
||||||
val isRetry: Boolean = failureCount != 0
|
val isRetry: Boolean = failureCount != 0
|
||||||
val (message, proto) = MessageReceiver.parse(this.data, this.openGroupMessageServerID, isRetry)
|
val (message, proto) = MessageReceiver.parse(this.data, this.openGroupMessageServerID, isRetry)
|
||||||
synchronized(RECEIVE_LOCK) {
|
synchronized(RECEIVE_LOCK) { // FIXME: Do we need this?
|
||||||
MessageReceiver.handle(message, proto, this.openGroupID)
|
MessageReceiver.handle(message, proto, this.openGroupID)
|
||||||
}
|
}
|
||||||
this.handleSuccess()
|
this.handleSuccess()
|
||||||
deferred.resolve(Unit)
|
deferred.resolve(Unit)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Couldn't receive message due to error", e)
|
Log.e(TAG, "Couldn't receive message.", e)
|
||||||
val error = e as? MessageReceiver.Error
|
if (e is MessageReceiver.Error && !e.isRetryable) {
|
||||||
if (error != null && !error.isRetryable) {
|
Log.e("Loki", "Message receive job permanently failed.", e)
|
||||||
Log.e("Loki", "Message receive job permanently failed due to error", e)
|
this.handlePermanentFailure(e)
|
||||||
this.handlePermanentFailure(error)
|
|
||||||
} else {
|
} else {
|
||||||
Log.e("Loki", "Couldn't receive message due to error", e)
|
Log.e("Loki", "Couldn't receive message.", e)
|
||||||
this.handleFailure(e)
|
this.handleFailure(e)
|
||||||
}
|
}
|
||||||
deferred.resolve(Unit) // The promise is just used to keep track of when we're done
|
deferred.resolve(Unit) // The promise is just used to keep track of when we're done
|
||||||
@ -68,10 +67,10 @@ class MessageReceiveJob(val data: ByteArray, val isBackgroundPoll: Boolean, val
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun serialize(): Data {
|
override fun serialize(): Data {
|
||||||
val builder = Data.Builder().putByteArray(KEY_DATA, data)
|
val builder = Data.Builder().putByteArray(DATA_KEY, data)
|
||||||
.putBoolean(KEY_IS_BACKGROUND_POLL, isBackgroundPoll)
|
.putBoolean(IS_BACKGROUND_POLL_KEY, isBackgroundPoll)
|
||||||
openGroupMessageServerID?.let { builder.putLong(KEY_OPEN_GROUP_MESSAGE_SERVER_ID, openGroupMessageServerID) }
|
openGroupMessageServerID?.let { builder.putLong(OPEN_GROUP_MESSAGE_SERVER_ID_KEY, it) }
|
||||||
openGroupID?.let { builder.putString(KEY_OPEN_GROUP_ID, openGroupID) }
|
openGroupID?.let { builder.putString(OPEN_GROUP_ID_KEY, it) }
|
||||||
return builder.build();
|
return builder.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,7 +81,12 @@ class MessageReceiveJob(val data: ByteArray, val isBackgroundPoll: Boolean, val
|
|||||||
class Factory: Job.Factory<MessageReceiveJob> {
|
class Factory: Job.Factory<MessageReceiveJob> {
|
||||||
|
|
||||||
override fun create(data: Data): MessageReceiveJob {
|
override fun create(data: Data): MessageReceiveJob {
|
||||||
return MessageReceiveJob(data.getByteArray(KEY_DATA), data.getBoolean(KEY_IS_BACKGROUND_POLL), data.getLong(KEY_OPEN_GROUP_MESSAGE_SERVER_ID), data.getString(KEY_OPEN_GROUP_ID))
|
return MessageReceiveJob(
|
||||||
|
data.getByteArray(DATA_KEY),
|
||||||
|
data.getBoolean(IS_BACKGROUND_POLL_KEY),
|
||||||
|
data.getLong(OPEN_GROUP_MESSAGE_SERVER_ID_KEY),
|
||||||
|
data.getString(OPEN_GROUP_ID_KEY)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -16,22 +16,22 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job {
|
|||||||
override var id: String? = null
|
override var id: String? = null
|
||||||
override var failureCount: Int = 0
|
override var failureCount: Int = 0
|
||||||
|
|
||||||
// Settings
|
|
||||||
override val maxFailureCount: Int = 10
|
override val maxFailureCount: Int = 10
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val TAG = MessageSendJob::class.simpleName
|
val TAG = MessageSendJob::class.simpleName
|
||||||
val KEY: String = "MessageSendJob"
|
val KEY: String = "MessageSendJob"
|
||||||
|
|
||||||
// Keys used for database storage
|
// Keys used for database storage
|
||||||
private val KEY_MESSAGE = "message"
|
private val MESSAGE_KEY = "message"
|
||||||
private val KEY_DESTINATION = "destination"
|
private val DESTINATION_KEY = "destination"
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun execute() {
|
override fun execute() {
|
||||||
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
|
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
|
||||||
val message = message as? VisibleMessage
|
val message = message as? VisibleMessage
|
||||||
message?.let {
|
if (message != null) {
|
||||||
if(!messageDataProvider.isOutgoingMessage(message.sentTimestamp!!)) return // The message has been deleted
|
if (!messageDataProvider.isOutgoingMessage(message.sentTimestamp!!)) return // The message has been deleted
|
||||||
val attachmentIDs = mutableListOf<Long>()
|
val attachmentIDs = mutableListOf<Long>()
|
||||||
attachmentIDs.addAll(message.attachmentIDs)
|
attachmentIDs.addAll(message.attachmentIDs)
|
||||||
message.quote?.let { it.attachmentID?.let { attachmentID -> attachmentIDs.add(attachmentID) } }
|
message.quote?.let { it.attachmentID?.let { attachmentID -> attachmentIDs.add(attachmentID) } }
|
||||||
@ -52,9 +52,8 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job {
|
|||||||
this.handleSuccess()
|
this.handleSuccess()
|
||||||
}.fail { exception ->
|
}.fail { exception ->
|
||||||
Log.e(TAG, "Couldn't send message due to error: $exception.")
|
Log.e(TAG, "Couldn't send message due to error: $exception.")
|
||||||
val e = exception as? MessageSender.Error
|
if (exception is MessageSender.Error) {
|
||||||
e?.let {
|
if (!exception.isRetryable) { this.handlePermanentFailure(exception) }
|
||||||
if (!e.isRetryable) this.handlePermanentFailure(e)
|
|
||||||
}
|
}
|
||||||
this.handleFailure(exception)
|
this.handleFailure(exception)
|
||||||
}
|
}
|
||||||
@ -71,8 +70,10 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job {
|
|||||||
private fun handleFailure(error: Exception) {
|
private fun handleFailure(error: Exception) {
|
||||||
Log.w(TAG, "Failed to send $message::class.simpleName.")
|
Log.w(TAG, "Failed to send $message::class.simpleName.")
|
||||||
val message = message as? VisibleMessage
|
val message = message as? VisibleMessage
|
||||||
message?.let {
|
if (message != null) {
|
||||||
if(!MessagingModuleConfiguration.shared.messageDataProvider.isOutgoingMessage(message.sentTimestamp!!)) return // The message has been deleted
|
if (!MessagingModuleConfiguration.shared.messageDataProvider.isOutgoingMessage(message.sentTimestamp!!)) {
|
||||||
|
return // The message has been deleted
|
||||||
|
}
|
||||||
}
|
}
|
||||||
delegate?.handleJobFailed(this, error)
|
delegate?.handleJobFailed(this, error)
|
||||||
}
|
}
|
||||||
@ -81,40 +82,54 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job {
|
|||||||
val kryo = Kryo()
|
val kryo = Kryo()
|
||||||
kryo.isRegistrationRequired = false
|
kryo.isRegistrationRequired = false
|
||||||
val output = Output(ByteArray(4096), MAX_BUFFER_SIZE)
|
val output = Output(ByteArray(4096), MAX_BUFFER_SIZE)
|
||||||
|
// Message
|
||||||
kryo.writeClassAndObject(output, message)
|
kryo.writeClassAndObject(output, message)
|
||||||
output.close()
|
output.close()
|
||||||
val serializedMessage = output.toBytes()
|
val serializedMessage = output.toBytes()
|
||||||
output.clear()
|
output.clear()
|
||||||
|
// Destination
|
||||||
kryo.writeClassAndObject(output, destination)
|
kryo.writeClassAndObject(output, destination)
|
||||||
output.close()
|
output.close()
|
||||||
val serializedDestination = output.toBytes()
|
val serializedDestination = output.toBytes()
|
||||||
return Data.Builder().putByteArray(KEY_MESSAGE, serializedMessage)
|
output.clear()
|
||||||
.putByteArray(KEY_DESTINATION, serializedDestination)
|
// Serialize
|
||||||
.build();
|
return Data.Builder()
|
||||||
|
.putByteArray(MESSAGE_KEY, serializedMessage)
|
||||||
|
.putByteArray(DESTINATION_KEY, serializedDestination)
|
||||||
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getFactoryKey(): String {
|
override fun getFactoryKey(): String {
|
||||||
return KEY
|
return KEY
|
||||||
}
|
}
|
||||||
|
|
||||||
class Factory: Job.Factory<MessageSendJob> {
|
class Factory : Job.Factory<MessageSendJob> {
|
||||||
|
|
||||||
override fun create(data: Data): MessageSendJob {
|
override fun create(data: Data): MessageSendJob? {
|
||||||
val serializedMessage = data.getByteArray(KEY_MESSAGE)
|
val serializedMessage = data.getByteArray(MESSAGE_KEY)
|
||||||
val serializedDestination = data.getByteArray(KEY_DESTINATION)
|
val serializedDestination = data.getByteArray(DESTINATION_KEY)
|
||||||
val kryo = Kryo()
|
val kryo = Kryo()
|
||||||
kryo.isRegistrationRequired = false
|
// Message
|
||||||
var input = Input(serializedMessage)
|
val messageInput = Input(serializedMessage)
|
||||||
val messageClass = kryo.readClass(input)
|
val message: Message
|
||||||
if (messageClass == null || !Message::class.java.isAssignableFrom(messageClass.type)) {
|
try {
|
||||||
// if the message class doesn't exist or it doesn't implement `Message` parent class
|
message = kryo.readClassAndObject(messageInput) as Message
|
||||||
throw Exception("deserialized messageClass was ${messageClass.type}")
|
} catch (e: Exception) {
|
||||||
|
Log.e("Loki", "Couldn't deserialize message send job.", e)
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
val message = kryo.readClassAndObject(input) as Message
|
messageInput.close()
|
||||||
input.close()
|
// Destination
|
||||||
input = Input(serializedDestination)
|
val destinationInput = Input(serializedDestination)
|
||||||
val destination = kryo.readClassAndObject(input) as Destination
|
val destination: Destination
|
||||||
input.close()
|
try {
|
||||||
|
destination = kryo.readClassAndObject(destinationInput) as Destination
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("Loki", "Couldn't deserialize message send job.", e)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
destinationInput.close()
|
||||||
|
// Return
|
||||||
return MessageSendJob(message, destination)
|
return MessageSendJob(message, destination)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,16 +21,14 @@ class NotifyPNServerJob(val message: SnodeMessage) : Job {
|
|||||||
override var id: String? = null
|
override var id: String? = null
|
||||||
override var failureCount: Int = 0
|
override var failureCount: Int = 0
|
||||||
|
|
||||||
// Settings
|
|
||||||
override val maxFailureCount: Int = 20
|
override val maxFailureCount: Int = 20
|
||||||
companion object {
|
companion object {
|
||||||
val KEY: String = "NotifyPNServerJob"
|
val KEY: String = "NotifyPNServerJob"
|
||||||
|
|
||||||
// Keys used for database storage
|
// Keys used for database storage
|
||||||
private val KEY_MESSAGE = "message"
|
private val MESSAGE_KEY = "message"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Running
|
|
||||||
override fun execute() {
|
override fun execute() {
|
||||||
val server = PushNotificationAPI.server
|
val server = PushNotificationAPI.server
|
||||||
val parameters = mapOf( "data" to message.data, "send_to" to message.recipient )
|
val parameters = mapOf( "data" to message.data, "send_to" to message.recipient )
|
||||||
@ -41,10 +39,10 @@ class NotifyPNServerJob(val message: SnodeMessage) : Job {
|
|||||||
OnionRequestAPI.sendOnionRequest(request.build(), server, PushNotificationAPI.serverPublicKey, "/loki/v2/lsrpc").map { json ->
|
OnionRequestAPI.sendOnionRequest(request.build(), server, PushNotificationAPI.serverPublicKey, "/loki/v2/lsrpc").map { json ->
|
||||||
val code = json["code"] as? Int
|
val code = json["code"] as? Int
|
||||||
if (code == null || code == 0) {
|
if (code == null || code == 0) {
|
||||||
Log.d("Loki", "[Loki] Couldn't notify PN server due to error: ${json["message"] as? String ?: "null"}.")
|
Log.d("Loki", "Couldn't notify PN server due to error: ${json["message"] as? String ?: "null"}.")
|
||||||
}
|
}
|
||||||
}.fail { exception ->
|
}.fail { exception ->
|
||||||
Log.d("Loki", "[Loki] Couldn't notify PN server due to error: $exception.")
|
Log.d("Loki", "Couldn't notify PN server due to error: $exception.")
|
||||||
}
|
}
|
||||||
}.success {
|
}.success {
|
||||||
handleSuccess()
|
handleSuccess()
|
||||||
@ -68,17 +66,17 @@ class NotifyPNServerJob(val message: SnodeMessage) : Job {
|
|||||||
val output = Output(serializedMessage)
|
val output = Output(serializedMessage)
|
||||||
kryo.writeObject(output, message)
|
kryo.writeObject(output, message)
|
||||||
output.close()
|
output.close()
|
||||||
return Data.Builder().putByteArray(KEY_MESSAGE, serializedMessage).build();
|
return Data.Builder().putByteArray(MESSAGE_KEY, serializedMessage).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getFactoryKey(): String {
|
override fun getFactoryKey(): String {
|
||||||
return KEY
|
return KEY
|
||||||
}
|
}
|
||||||
|
|
||||||
class Factory: Job.Factory<NotifyPNServerJob> {
|
class Factory : Job.Factory<NotifyPNServerJob> {
|
||||||
|
|
||||||
override fun create(data: Data): NotifyPNServerJob {
|
override fun create(data: Data): NotifyPNServerJob {
|
||||||
val serializedMessage = data.getByteArray(KEY_MESSAGE)
|
val serializedMessage = data.getByteArray(MESSAGE_KEY)
|
||||||
val kryo = Kryo()
|
val kryo = Kryo()
|
||||||
kryo.isRegistrationRequired = false
|
kryo.isRegistrationRequired = false
|
||||||
val input = Input(serializedMessage)
|
val input = Input(serializedMessage)
|
||||||
|
@ -2,11 +2,11 @@ package org.session.libsession.messaging.jobs
|
|||||||
|
|
||||||
class SessionJobInstantiator(private val jobFactories: Map<String, Job.Factory<out Job>>) {
|
class SessionJobInstantiator(private val jobFactories: Map<String, Job.Factory<out Job>>) {
|
||||||
|
|
||||||
fun instantiate(jobFactoryKey: String, data: Data): Job {
|
fun instantiate(jobFactoryKey: String, data: Data): Job? {
|
||||||
if (jobFactories.containsKey(jobFactoryKey)) {
|
if (jobFactories.containsKey(jobFactoryKey)) {
|
||||||
return jobFactories[jobFactoryKey]?.create(data) ?: throw IllegalStateException("Tried to instantiate a job with key '$jobFactoryKey', but no matching factory was found.")
|
return jobFactories[jobFactoryKey]?.create(data)
|
||||||
} else {
|
} else {
|
||||||
throw IllegalStateException("Tried to instantiate a job with key '$jobFactoryKey', but no matching factory was found.")
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -3,6 +3,7 @@ package org.session.libsession.messaging.jobs
|
|||||||
class SessionJobManagerFactories {
|
class SessionJobManagerFactories {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
fun getSessionJobFactories(): Map<String, Job.Factory<out Job>> {
|
fun getSessionJobFactories(): Map<String, Job.Factory<out Job>> {
|
||||||
return mapOf(
|
return mapOf(
|
||||||
AttachmentDownloadJob.KEY to AttachmentDownloadJob.Factory(),
|
AttachmentDownloadJob.KEY to AttachmentDownloadJob.Factory(),
|
||||||
|
@ -7,9 +7,6 @@ import org.session.libsession.messaging.threads.Address
|
|||||||
import org.session.libsession.utilities.GroupUtil
|
import org.session.libsession.utilities.GroupUtil
|
||||||
import org.session.libsignal.service.loki.utilities.toHexString
|
import org.session.libsignal.service.loki.utilities.toHexString
|
||||||
|
|
||||||
typealias OpenGroupModel = OpenGroup
|
|
||||||
typealias OpenGroupV2Model = OpenGroupV2
|
|
||||||
|
|
||||||
sealed class Destination {
|
sealed class Destination {
|
||||||
|
|
||||||
class Contact(var publicKey: String) : Destination() {
|
class Contact(var publicKey: String) : Destination() {
|
||||||
@ -21,11 +18,12 @@ sealed class Destination {
|
|||||||
class OpenGroup(var channel: Long, var server: String) : Destination() {
|
class OpenGroup(var channel: Long, var server: String) : Destination() {
|
||||||
internal constructor(): this(0, "")
|
internal constructor(): this(0, "")
|
||||||
}
|
}
|
||||||
class OpenGroupV2(var room: String, var server: String): Destination() {
|
class OpenGroupV2(var room: String, var server: String) : Destination() {
|
||||||
internal constructor(): this("", "")
|
internal constructor(): this("", "")
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
fun from(address: Address): Destination {
|
fun from(address: Address): Destination {
|
||||||
return when {
|
return when {
|
||||||
address.isContact -> {
|
address.isContact -> {
|
||||||
@ -39,10 +37,12 @@ sealed class Destination {
|
|||||||
address.isOpenGroup -> {
|
address.isOpenGroup -> {
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
val threadID = storage.getThreadID(address.contactIdentifier())!!
|
val threadID = storage.getThreadID(address.contactIdentifier())!!
|
||||||
when (val openGroup = storage.getOpenGroup(threadID) ?: storage.getV2OpenGroup(threadID)) {
|
when (val openGroup = storage.getV2OpenGroup(threadID) ?: storage.getOpenGroup(threadID)) {
|
||||||
is OpenGroupModel -> OpenGroup(openGroup.channel, openGroup.server)
|
is org.session.libsession.messaging.open_groups.OpenGroup
|
||||||
is OpenGroupV2Model -> OpenGroupV2(openGroup.room, openGroup.server)
|
-> Destination.OpenGroup(openGroup.channel, openGroup.server)
|
||||||
else -> throw Exception("Invalid OpenGroup $openGroup")
|
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.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
|
@ -18,12 +18,10 @@ abstract class Message {
|
|||||||
open val isSelfSendValid: Boolean = false
|
open val isSelfSendValid: Boolean = false
|
||||||
|
|
||||||
open fun isValid(): Boolean {
|
open fun isValid(): Boolean {
|
||||||
sentTimestamp?.let {
|
val sentTimestamp = sentTimestamp
|
||||||
if (it <= 0) return false
|
if (sentTimestamp != null && sentTimestamp <= 0) { return false }
|
||||||
}
|
val receivedTimestamp = receivedTimestamp
|
||||||
receivedTimestamp?.let {
|
if (receivedTimestamp != null && receivedTimestamp <= 0) { return false }
|
||||||
if (it <= 0) return false
|
|
||||||
}
|
|
||||||
return sender != null && recipient != null
|
return sender != null && recipient != null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,9 +16,10 @@ import org.session.libsignal.utilities.Hex
|
|||||||
import org.session.libsignal.utilities.logging.Log
|
import org.session.libsignal.utilities.logging.Log
|
||||||
|
|
||||||
class ClosedGroupControlMessage() : ControlMessage() {
|
class ClosedGroupControlMessage() : ControlMessage() {
|
||||||
|
var kind: Kind? = null
|
||||||
|
|
||||||
override val ttl: Long = run {
|
override val ttl: Long get() {
|
||||||
when (kind) {
|
return when (kind) {
|
||||||
is Kind.EncryptionKeyPair -> 14 * 24 * 60 * 60 * 1000
|
is Kind.EncryptionKeyPair -> 14 * 24 * 60 * 60 * 1000
|
||||||
else -> 14 * 24 * 60 * 60 * 1000
|
else -> 14 * 24 * 60 * 60 * 1000
|
||||||
}
|
}
|
||||||
@ -26,31 +27,46 @@ class ClosedGroupControlMessage() : ControlMessage() {
|
|||||||
|
|
||||||
override val isSelfSendValid: Boolean = true
|
override val isSelfSendValid: Boolean = true
|
||||||
|
|
||||||
var kind: Kind? = null
|
override fun isValid(): Boolean {
|
||||||
|
val kind = kind
|
||||||
|
if (!super.isValid() || kind == null) return false
|
||||||
|
return when (kind) {
|
||||||
|
is Kind.New -> {
|
||||||
|
!kind.publicKey.isEmpty && kind.name.isNotEmpty() && kind.encryptionKeyPair?.publicKey != null
|
||||||
|
&& kind.encryptionKeyPair?.privateKey != null && kind.members.isNotEmpty() && kind.admins.isNotEmpty()
|
||||||
|
}
|
||||||
|
is Kind.EncryptionKeyPair -> true
|
||||||
|
is Kind.NameChange -> kind.name.isNotEmpty()
|
||||||
|
is Kind.MembersAdded -> kind.members.isNotEmpty()
|
||||||
|
is Kind.MembersRemoved -> kind.members.isNotEmpty()
|
||||||
|
is Kind.MemberLeft -> true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
sealed class Kind {
|
sealed class Kind {
|
||||||
class New(var publicKey: ByteString, var name: String, var encryptionKeyPair: ECKeyPair?, var members: List<ByteString>, var admins: List<ByteString>) : Kind() {
|
class New(var publicKey: ByteString, var name: String, var encryptionKeyPair: ECKeyPair?, var members: List<ByteString>, var admins: List<ByteString>) : Kind() {
|
||||||
internal constructor(): this(ByteString.EMPTY, "", null, listOf(), listOf())
|
internal constructor() : this(ByteString.EMPTY, "", null, listOf(), listOf())
|
||||||
}
|
}
|
||||||
/// An encryption key pair encrypted for each member individually.
|
/** An encryption key pair encrypted for each member individually.
|
||||||
///
|
*
|
||||||
/// - Note: `publicKey` is only set when an encryption key pair is sent in a one-to-one context (i.e. not in a group).
|
* **Note:** `publicKey` is only set when an encryption key pair is sent in a one-to-one context (i.e. not in a group).
|
||||||
|
*/
|
||||||
class EncryptionKeyPair(var publicKey: ByteString?, var wrappers: Collection<KeyPairWrapper>) : Kind() {
|
class EncryptionKeyPair(var publicKey: ByteString?, var wrappers: Collection<KeyPairWrapper>) : Kind() {
|
||||||
internal constructor(): this(null, listOf())
|
internal constructor() : this(null, listOf())
|
||||||
}
|
}
|
||||||
class NameChange(var name: String) : Kind() {
|
class NameChange(var name: String) : Kind() {
|
||||||
internal constructor(): this("")
|
internal constructor() : this("")
|
||||||
}
|
}
|
||||||
class MembersAdded(var members: List<ByteString>) : Kind() {
|
class MembersAdded(var members: List<ByteString>) : Kind() {
|
||||||
internal constructor(): this(listOf())
|
internal constructor() : this(listOf())
|
||||||
}
|
}
|
||||||
class MembersRemoved(var members: List<ByteString>) : Kind() {
|
class MembersRemoved(var members: List<ByteString>) : Kind() {
|
||||||
internal constructor(): this(listOf())
|
internal constructor() : this(listOf())
|
||||||
}
|
}
|
||||||
class MemberLeft() : Kind()
|
class MemberLeft() : Kind()
|
||||||
|
|
||||||
val description: String =
|
val description: String =
|
||||||
when(this) {
|
when (this) {
|
||||||
is New -> "new"
|
is New -> "new"
|
||||||
is EncryptionKeyPair -> "encryptionKeyPair"
|
is EncryptionKeyPair -> "encryptionKeyPair"
|
||||||
is NameChange -> "nameChange"
|
is NameChange -> "nameChange"
|
||||||
@ -65,18 +81,19 @@ class ClosedGroupControlMessage() : ControlMessage() {
|
|||||||
|
|
||||||
fun fromProto(proto: SignalServiceProtos.Content): ClosedGroupControlMessage? {
|
fun fromProto(proto: SignalServiceProtos.Content): ClosedGroupControlMessage? {
|
||||||
if (!proto.hasDataMessage() || !proto.dataMessage.hasClosedGroupControlMessage()) return null
|
if (!proto.hasDataMessage() || !proto.dataMessage.hasClosedGroupControlMessage()) return null
|
||||||
val closedGroupControlMessageProto = proto.dataMessage?.closedGroupControlMessage!!
|
val closedGroupControlMessageProto = proto.dataMessage!!.closedGroupControlMessage!!
|
||||||
val kind: Kind
|
val kind: Kind
|
||||||
when (closedGroupControlMessageProto.type) {
|
when (closedGroupControlMessageProto.type!!) {
|
||||||
DataMessage.ClosedGroupControlMessage.Type.NEW -> {
|
DataMessage.ClosedGroupControlMessage.Type.NEW -> {
|
||||||
val publicKey = closedGroupControlMessageProto.publicKey ?: return null
|
val publicKey = closedGroupControlMessageProto.publicKey ?: return null
|
||||||
val name = closedGroupControlMessageProto.name ?: return null
|
val name = closedGroupControlMessageProto.name ?: return null
|
||||||
val encryptionKeyPairAsProto = closedGroupControlMessageProto.encryptionKeyPair ?: return null
|
val encryptionKeyPairAsProto = closedGroupControlMessageProto.encryptionKeyPair ?: return null
|
||||||
try {
|
try {
|
||||||
val encryptionKeyPair = ECKeyPair(DjbECPublicKey(encryptionKeyPairAsProto.publicKey.toByteArray()), DjbECPrivateKey(encryptionKeyPairAsProto.privateKey.toByteArray()))
|
val encryptionKeyPair = ECKeyPair(DjbECPublicKey(encryptionKeyPairAsProto.publicKey.toByteArray()),
|
||||||
|
DjbECPrivateKey(encryptionKeyPairAsProto.privateKey.toByteArray()))
|
||||||
kind = Kind.New(publicKey, name, encryptionKeyPair, closedGroupControlMessageProto.membersList, closedGroupControlMessageProto.adminsList)
|
kind = Kind.New(publicKey, name, encryptionKeyPair, closedGroupControlMessageProto.membersList, closedGroupControlMessageProto.adminsList)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(TAG, "Couldn't parse key pair")
|
Log.w(TAG, "Couldn't parse key pair from proto: $encryptionKeyPairAsProto.")
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -107,26 +124,10 @@ class ClosedGroupControlMessage() : ControlMessage() {
|
|||||||
this.kind = kind
|
this.kind = kind
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isValid(): Boolean {
|
|
||||||
if (!super.isValid()) return false
|
|
||||||
val kind = kind ?: return false
|
|
||||||
return when(kind) {
|
|
||||||
is Kind.New -> {
|
|
||||||
!kind.publicKey.isEmpty && kind.name.isNotEmpty() && kind.encryptionKeyPair!!.publicKey != null
|
|
||||||
&& kind.encryptionKeyPair!!.privateKey != null && kind.members.isNotEmpty() && kind.admins.isNotEmpty()
|
|
||||||
}
|
|
||||||
is Kind.EncryptionKeyPair -> true
|
|
||||||
is Kind.NameChange -> kind.name.isNotEmpty()
|
|
||||||
is Kind.MembersAdded -> kind.members.isNotEmpty()
|
|
||||||
is Kind.MembersRemoved -> kind.members.isNotEmpty()
|
|
||||||
is Kind.MemberLeft -> true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toProto(): SignalServiceProtos.Content? {
|
override fun toProto(): SignalServiceProtos.Content? {
|
||||||
val kind = kind
|
val kind = kind
|
||||||
if (kind == null) {
|
if (kind == null) {
|
||||||
Log.w(TAG, "Couldn't construct closed group update proto from: $this")
|
Log.w(TAG, "Couldn't construct closed group control message proto from: $this.")
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@ -176,7 +177,7 @@ class ClosedGroupControlMessage() : ControlMessage() {
|
|||||||
contentProto.dataMessage = dataMessageProto.build()
|
contentProto.dataMessage = dataMessageProto.build()
|
||||||
return contentProto.build()
|
return contentProto.build()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(TAG, "Couldn't construct closed group update proto from: $this")
|
Log.w(TAG, "Couldn't construct closed group control message proto from: $this.")
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -188,6 +189,7 @@ class ClosedGroupControlMessage() : ControlMessage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
fun fromProto(proto: DataMessage.ClosedGroupControlMessage.KeyPairWrapper): KeyPairWrapper {
|
fun fromProto(proto: DataMessage.ClosedGroupControlMessage.KeyPairWrapper): KeyPairWrapper {
|
||||||
return KeyPairWrapper(proto.publicKey.toByteArray().toHexString(), proto.encryptedKeyPair)
|
return KeyPairWrapper(proto.publicKey.toByteArray().toHexString(), proto.encryptedKeyPair)
|
||||||
}
|
}
|
||||||
@ -199,7 +201,6 @@ class ClosedGroupControlMessage() : ControlMessage() {
|
|||||||
val result = DataMessage.ClosedGroupControlMessage.KeyPairWrapper.newBuilder()
|
val result = DataMessage.ClosedGroupControlMessage.KeyPairWrapper.newBuilder()
|
||||||
result.publicKey = ByteString.copyFrom(Hex.fromStringCondensed(publicKey))
|
result.publicKey = ByteString.copyFrom(Hex.fromStringCondensed(publicKey))
|
||||||
result.encryptedKeyPair = encryptedKeyPair
|
result.encryptedKeyPair = encryptedKeyPair
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
result.build()
|
result.build()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
@ -14,12 +14,15 @@ import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
|
|||||||
import org.session.libsignal.service.loki.utilities.toHexString
|
import org.session.libsignal.service.loki.utilities.toHexString
|
||||||
import org.session.libsignal.utilities.Hex
|
import org.session.libsignal.utilities.Hex
|
||||||
|
|
||||||
class ConfigurationMessage(var closedGroups: List<ClosedGroup>, var openGroups: List<String>, var contacts: List<Contact>, var displayName: String, var profilePicture: String?, var profileKey: ByteArray): ControlMessage() {
|
class ConfigurationMessage(var closedGroups: List<ClosedGroup>, var openGroups: List<String>, var contacts: List<Contact>,
|
||||||
|
var displayName: String, var profilePicture: String?, var profileKey: ByteArray) : ControlMessage() {
|
||||||
|
|
||||||
|
override val isSelfSendValid: Boolean = true
|
||||||
|
|
||||||
class ClosedGroup(var publicKey: String, var name: String, var encryptionKeyPair: ECKeyPair?, var members: List<String>, var admins: List<String>) {
|
class ClosedGroup(var publicKey: String, var name: String, var encryptionKeyPair: ECKeyPair?, var members: List<String>, var admins: List<String>) {
|
||||||
val isValid: Boolean get() = members.isNotEmpty() && admins.isNotEmpty()
|
val isValid: Boolean get() = members.isNotEmpty() && admins.isNotEmpty()
|
||||||
|
|
||||||
internal constructor(): this("", "", null, listOf(), listOf())
|
internal constructor() : this("", "", null, listOf(), listOf())
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
return name
|
return name
|
||||||
@ -56,7 +59,7 @@ class ConfigurationMessage(var closedGroups: List<ClosedGroup>, var openGroups:
|
|||||||
|
|
||||||
class Contact(var publicKey: String, var name: String, var profilePicture: String?, var profileKey: ByteArray?) {
|
class Contact(var publicKey: String, var name: String, var profilePicture: String?, var profileKey: ByteArray?) {
|
||||||
|
|
||||||
internal constructor(): this("", "", null, null)
|
internal constructor() : this("", "", null, null)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
@ -66,8 +69,7 @@ class ConfigurationMessage(var closedGroups: List<ClosedGroup>, var openGroups:
|
|||||||
val name = proto.name
|
val name = proto.name
|
||||||
val profilePicture = if (proto.hasProfilePicture()) proto.profilePicture else null
|
val profilePicture = if (proto.hasProfilePicture()) proto.profilePicture else null
|
||||||
val profileKey = if (proto.hasProfileKey()) proto.profileKey.toByteArray() else null
|
val profileKey = if (proto.hasProfileKey()) proto.profileKey.toByteArray() else null
|
||||||
|
return Contact(publicKey, name, profilePicture, profileKey)
|
||||||
return Contact(publicKey,name,profilePicture,profileKey)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,18 +81,18 @@ class ConfigurationMessage(var closedGroups: List<ClosedGroup>, var openGroups:
|
|||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
if (!this.profilePicture.isNullOrEmpty()) {
|
val profilePicture = profilePicture
|
||||||
result.profilePicture = this.profilePicture
|
if (!profilePicture.isNullOrEmpty()) {
|
||||||
|
result.profilePicture = profilePicture
|
||||||
}
|
}
|
||||||
if (this.profileKey != null) {
|
val profileKey = profileKey
|
||||||
result.profileKey = ByteString.copyFrom(this.profileKey)
|
if (profileKey != null) {
|
||||||
|
result.profileKey = ByteString.copyFrom(profileKey)
|
||||||
}
|
}
|
||||||
return result.build()
|
return result.build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override val isSelfSendValid: Boolean = true
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
fun getCurrent(contacts: List<Contact>): ConfigurationMessage? {
|
fun getCurrent(contacts: List<Contact>): ConfigurationMessage? {
|
||||||
@ -103,24 +105,22 @@ class ConfigurationMessage(var closedGroups: List<ClosedGroup>, var openGroups:
|
|||||||
val profilePicture = TextSecurePreferences.getProfilePictureURL(context)
|
val profilePicture = TextSecurePreferences.getProfilePictureURL(context)
|
||||||
val profileKey = ProfileKeyUtil.getProfileKey(context)
|
val profileKey = ProfileKeyUtil.getProfileKey(context)
|
||||||
val groups = storage.getAllGroups()
|
val groups = storage.getAllGroups()
|
||||||
for (groupRecord in groups) {
|
for (group in groups) {
|
||||||
if (groupRecord.isClosedGroup) {
|
if (group.isClosedGroup) {
|
||||||
if (!groupRecord.members.contains(Address.fromSerialized(storage.getUserPublicKey()!!))) continue
|
if (!group.members.contains(Address.fromSerialized(storage.getUserPublicKey()!!))) continue
|
||||||
val groupPublicKey = GroupUtil.doubleDecodeGroupID(groupRecord.encodedId).toHexString()
|
val groupPublicKey = GroupUtil.doubleDecodeGroupID(group.encodedId).toHexString()
|
||||||
val encryptionKeyPair = storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) ?: continue
|
val encryptionKeyPair = storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) ?: continue
|
||||||
val closedGroup = ClosedGroup(groupPublicKey, groupRecord.title, encryptionKeyPair, groupRecord.members.map { it.serialize() }, groupRecord.admins.map { it.serialize() })
|
val closedGroup = ClosedGroup(groupPublicKey, group.title, encryptionKeyPair, group.members.map { it.serialize() }, group.admins.map { it.serialize() })
|
||||||
closedGroups.add(closedGroup)
|
closedGroups.add(closedGroup)
|
||||||
}
|
}
|
||||||
if (groupRecord.isOpenGroup) {
|
if (group.isOpenGroup) {
|
||||||
val threadID = storage.getThreadID(groupRecord.encodedId) ?: continue
|
val threadID = storage.getThreadID(group.encodedId) ?: continue
|
||||||
val openGroup = storage.getOpenGroup(threadID)
|
val openGroup = storage.getOpenGroup(threadID)
|
||||||
val openGroupV2 = storage.getV2OpenGroup(threadID)
|
val openGroupV2 = storage.getV2OpenGroup(threadID)
|
||||||
|
val shareUrl = openGroup?.server ?: openGroupV2?.joinURL ?: continue
|
||||||
val shareUrl = openGroup?.server ?: openGroupV2?.toJoinUrl() ?: continue
|
|
||||||
openGroups.add(shareUrl)
|
openGroups.add(shareUrl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ConfigurationMessage(closedGroups, openGroups, contacts, displayName, profilePicture, profileKey)
|
return ConfigurationMessage(closedGroups, openGroups, contacts, displayName, profilePicture, profileKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -145,6 +145,7 @@ class ConfigurationMessage(var closedGroups: List<ClosedGroup>, var openGroups:
|
|||||||
configurationProto.addAllOpenGroups(openGroups)
|
configurationProto.addAllOpenGroups(openGroups)
|
||||||
configurationProto.addAllContacts(this.contacts.mapNotNull { it.toProto() })
|
configurationProto.addAllContacts(this.contacts.mapNotNull { it.toProto() })
|
||||||
configurationProto.displayName = displayName
|
configurationProto.displayName = displayName
|
||||||
|
val profilePicture = profilePicture
|
||||||
if (!profilePicture.isNullOrEmpty()) {
|
if (!profilePicture.isNullOrEmpty()) {
|
||||||
configurationProto.profilePicture = profilePicture
|
configurationProto.profilePicture = profilePicture
|
||||||
}
|
}
|
||||||
@ -157,10 +158,10 @@ class ConfigurationMessage(var closedGroups: List<ClosedGroup>, var openGroups:
|
|||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
return """
|
return """
|
||||||
ConfigurationMessage(
|
ConfigurationMessage(
|
||||||
closedGroups: ${(closedGroups)}
|
closedGroups: ${(closedGroups)},
|
||||||
openGroups: ${(openGroups)}
|
openGroups: ${(openGroups)},
|
||||||
displayName: $displayName
|
displayName: $displayName,
|
||||||
profilePicture: $profilePicture
|
profilePicture: $profilePicture,
|
||||||
profileKey: $profileKey
|
profileKey: $profileKey
|
||||||
)
|
)
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
|
@ -2,5 +2,4 @@ package org.session.libsession.messaging.messages.control
|
|||||||
|
|
||||||
import org.session.libsession.messaging.messages.Message
|
import org.session.libsession.messaging.messages.Message
|
||||||
|
|
||||||
abstract class ControlMessage : Message() {
|
abstract class ControlMessage : Message()
|
||||||
}
|
|
@ -3,7 +3,7 @@ package org.session.libsession.messaging.messages.control
|
|||||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||||
import org.session.libsignal.utilities.logging.Log
|
import org.session.libsignal.utilities.logging.Log
|
||||||
|
|
||||||
class DataExtractionNotification(): ControlMessage() {
|
class DataExtractionNotification() : ControlMessage() {
|
||||||
var kind: Kind? = null
|
var kind: Kind? = null
|
||||||
|
|
||||||
sealed class Kind {
|
sealed class Kind {
|
||||||
@ -39,8 +39,8 @@ class DataExtractionNotification(): ControlMessage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun isValid(): Boolean {
|
override fun isValid(): Boolean {
|
||||||
if (!super.isValid()) return false
|
val kind = kind
|
||||||
val kind = kind ?: return false
|
if (!super.isValid() || kind == null) return false
|
||||||
return when(kind) {
|
return when(kind) {
|
||||||
is Kind.Screenshot -> true
|
is Kind.Screenshot -> true
|
||||||
is Kind.MediaSaved -> kind.timestamp > 0
|
is Kind.MediaSaved -> kind.timestamp > 0
|
||||||
|
@ -6,13 +6,20 @@ import org.session.libsignal.utilities.logging.Log
|
|||||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||||
|
|
||||||
class ExpirationTimerUpdate() : ControlMessage() {
|
class ExpirationTimerUpdate() : ControlMessage() {
|
||||||
/// In the case of a sync message, the public key of the person the message was targeted at.
|
/** In the case of a sync message, the public key of the person the message was targeted at.
|
||||||
/// - Note: `nil` if this isn't a sync message.
|
*
|
||||||
|
* **Note:** `nil` if this isn't a sync message.
|
||||||
|
*/
|
||||||
var syncTarget: String? = null
|
var syncTarget: String? = null
|
||||||
var duration: Int? = 0
|
var duration: Int? = 0
|
||||||
|
|
||||||
override val isSelfSendValid: Boolean = true
|
override val isSelfSendValid: Boolean = true
|
||||||
|
|
||||||
|
override fun isValid(): Boolean {
|
||||||
|
if (!super.isValid()) return false
|
||||||
|
return duration != null
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val TAG = "ExpirationTimerUpdate"
|
const val TAG = "ExpirationTimerUpdate"
|
||||||
|
|
||||||
@ -26,19 +33,14 @@ class ExpirationTimerUpdate() : ControlMessage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal constructor(syncTarget: String?, duration: Int) : this() {
|
|
||||||
this.syncTarget = syncTarget
|
|
||||||
this.duration = duration
|
|
||||||
}
|
|
||||||
|
|
||||||
internal constructor(duration: Int) : this() {
|
internal constructor(duration: Int) : this() {
|
||||||
this.syncTarget = null
|
this.syncTarget = null
|
||||||
this.duration = duration
|
this.duration = duration
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isValid(): Boolean {
|
internal constructor(syncTarget: String, duration: Int) : this() {
|
||||||
if (!super.isValid()) return false
|
this.syncTarget = syncTarget
|
||||||
return duration != null
|
this.duration = duration
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun toProto(): SignalServiceProtos.Content? {
|
override fun toProto(): SignalServiceProtos.Content? {
|
||||||
|
@ -6,6 +6,13 @@ import org.session.libsignal.utilities.logging.Log
|
|||||||
class ReadReceipt() : ControlMessage() {
|
class ReadReceipt() : ControlMessage() {
|
||||||
var timestamps: List<Long>? = null
|
var timestamps: List<Long>? = null
|
||||||
|
|
||||||
|
override fun isValid(): Boolean {
|
||||||
|
if (!super.isValid()) return false
|
||||||
|
val timestamps = timestamps ?: return false
|
||||||
|
if (timestamps.isNotEmpty()) { return true }
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val TAG = "ReadReceipt"
|
const val TAG = "ReadReceipt"
|
||||||
|
|
||||||
@ -22,13 +29,6 @@ class ReadReceipt() : ControlMessage() {
|
|||||||
this.timestamps = timestamps
|
this.timestamps = timestamps
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isValid(): Boolean {
|
|
||||||
if (!super.isValid()) return false
|
|
||||||
val timestamps = timestamps ?: return false
|
|
||||||
if (timestamps.isNotEmpty()) { return true }
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toProto(): SignalServiceProtos.Content? {
|
override fun toProto(): SignalServiceProtos.Content? {
|
||||||
val timestamps = timestamps
|
val timestamps = timestamps
|
||||||
if (timestamps == null) {
|
if (timestamps == null) {
|
||||||
|
@ -4,9 +4,15 @@ import org.session.libsignal.service.internal.push.SignalServiceProtos
|
|||||||
import org.session.libsignal.utilities.logging.Log
|
import org.session.libsignal.utilities.logging.Log
|
||||||
|
|
||||||
class TypingIndicator() : ControlMessage() {
|
class TypingIndicator() : ControlMessage() {
|
||||||
override val ttl: Long = 30 * 1000
|
|
||||||
var kind: Kind? = null
|
var kind: Kind? = null
|
||||||
|
|
||||||
|
override val ttl: Long = 20 * 1000
|
||||||
|
|
||||||
|
override fun isValid(): Boolean {
|
||||||
|
if (!super.isValid()) return false
|
||||||
|
return kind != null
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val TAG = "TypingIndicator"
|
const val TAG = "TypingIndicator"
|
||||||
|
|
||||||
@ -41,11 +47,6 @@ class TypingIndicator() : ControlMessage() {
|
|||||||
this.kind = kind
|
this.kind = kind
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isValid(): Boolean {
|
|
||||||
if (!super.isValid()) return false
|
|
||||||
return kind != null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toProto(): SignalServiceProtos.Content? {
|
override fun toProto(): SignalServiceProtos.Content? {
|
||||||
val timestamp = sentTimestamp
|
val timestamp = sentTimestamp
|
||||||
val kind = kind
|
val kind = kind
|
||||||
|
@ -10,6 +10,10 @@ class LinkPreview() {
|
|||||||
var url: String? = null
|
var url: String? = null
|
||||||
var attachmentID: Long? = 0
|
var attachmentID: Long? = 0
|
||||||
|
|
||||||
|
fun isValid(): Boolean {
|
||||||
|
return (title != null && url != null && attachmentID != null)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val TAG = "LinkPreview"
|
const val TAG = "LinkPreview"
|
||||||
|
|
||||||
@ -20,11 +24,8 @@ class LinkPreview() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun from(signalLinkPreview: SignalLinkPreiview?): LinkPreview? {
|
fun from(signalLinkPreview: SignalLinkPreiview?): LinkPreview? {
|
||||||
return if (signalLinkPreview == null) {
|
if (signalLinkPreview == null) { return null }
|
||||||
null
|
return LinkPreview(signalLinkPreview.title, signalLinkPreview.url, signalLinkPreview.attachmentId?.rowId)
|
||||||
} else {
|
|
||||||
LinkPreview(signalLinkPreview.title, signalLinkPreview.url, signalLinkPreview.attachmentId?.rowId)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -34,10 +35,6 @@ class LinkPreview() {
|
|||||||
this.attachmentID = attachmentID
|
this.attachmentID = attachmentID
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isValid(): Boolean {
|
|
||||||
return (title != null && url != null && attachmentID != null)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toProto(): SignalServiceProtos.DataMessage.Preview? {
|
fun toProto(): SignalServiceProtos.DataMessage.Preview? {
|
||||||
val url = url
|
val url = url
|
||||||
if (url == null) {
|
if (url == null) {
|
||||||
@ -46,10 +43,10 @@ class LinkPreview() {
|
|||||||
}
|
}
|
||||||
val linkPreviewProto = SignalServiceProtos.DataMessage.Preview.newBuilder()
|
val linkPreviewProto = SignalServiceProtos.DataMessage.Preview.newBuilder()
|
||||||
linkPreviewProto.url = url
|
linkPreviewProto.url = url
|
||||||
title?.let { linkPreviewProto.title = title }
|
title?.let { linkPreviewProto.title = it }
|
||||||
val attachmentID = attachmentID
|
val database = MessagingModuleConfiguration.shared.messageDataProvider
|
||||||
attachmentID?.let {
|
attachmentID?.let {
|
||||||
MessagingModuleConfiguration.shared.messageDataProvider.getSignalAttachmentPointer(attachmentID)?.let {
|
database.getSignalAttachmentPointer(it)?.let {
|
||||||
val attachmentProto = Attachment.createAttachmentPointer(it)
|
val attachmentProto = Attachment.createAttachmentPointer(it)
|
||||||
linkPreviewProto.image = attachmentProto
|
linkPreviewProto.image = attachmentProto
|
||||||
}
|
}
|
||||||
|
@ -17,12 +17,11 @@ class Profile() {
|
|||||||
val displayName = profileProto.displayName ?: return null
|
val displayName = profileProto.displayName ?: return null
|
||||||
val profileKey = proto.profileKey
|
val profileKey = proto.profileKey
|
||||||
val profilePictureURL = profileProto.profilePicture
|
val profilePictureURL = profileProto.profilePicture
|
||||||
profileKey?.let {
|
if (profileKey != null && profilePictureURL != null) {
|
||||||
profilePictureURL?.let {
|
return Profile(displayName, profileKey.toByteArray(), profilePictureURL)
|
||||||
return Profile(displayName = displayName, profileKey = profileKey.toByteArray(), profilePictureURL = profilePictureURL)
|
} else {
|
||||||
}
|
return Profile(displayName)
|
||||||
}
|
}
|
||||||
return Profile(displayName)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -35,16 +34,14 @@ class Profile() {
|
|||||||
fun toProto(): SignalServiceProtos.DataMessage? {
|
fun toProto(): SignalServiceProtos.DataMessage? {
|
||||||
val displayName = displayName
|
val displayName = displayName
|
||||||
if (displayName == null) {
|
if (displayName == null) {
|
||||||
Log.w(TAG, "Couldn't construct link preview proto from: $this")
|
Log.w(TAG, "Couldn't construct profile proto from: $this")
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
val dataMessageProto = SignalServiceProtos.DataMessage.newBuilder()
|
val dataMessageProto = SignalServiceProtos.DataMessage.newBuilder()
|
||||||
val profileProto = SignalServiceProtos.DataMessage.LokiProfile.newBuilder()
|
val profileProto = SignalServiceProtos.DataMessage.LokiProfile.newBuilder()
|
||||||
profileProto.displayName = displayName
|
profileProto.displayName = displayName
|
||||||
val profileKey = profileKey
|
profileKey?.let { dataMessageProto.profileKey = ByteString.copyFrom(it) }
|
||||||
profileKey?.let { dataMessageProto.profileKey = ByteString.copyFrom(profileKey) }
|
profilePictureURL?.let { profileProto.profilePicture = it }
|
||||||
val profilePictureURL = profilePictureURL
|
|
||||||
profilePictureURL?.let { profileProto.profilePicture = profilePictureURL }
|
|
||||||
// Build
|
// Build
|
||||||
try {
|
try {
|
||||||
dataMessageProto.profile = profileProto.build()
|
dataMessageProto.profile = profileProto.build()
|
||||||
|
@ -13,6 +13,10 @@ class Quote() {
|
|||||||
var text: String? = null
|
var text: String? = null
|
||||||
var attachmentID: Long? = null
|
var attachmentID: Long? = null
|
||||||
|
|
||||||
|
fun isValid(): Boolean {
|
||||||
|
return (timestamp != null && publicKey != null)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val TAG = "Quote"
|
const val TAG = "Quote"
|
||||||
|
|
||||||
@ -24,12 +28,9 @@ class Quote() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun from(signalQuote: SignalQuote?): Quote? {
|
fun from(signalQuote: SignalQuote?): Quote? {
|
||||||
return if (signalQuote == null) {
|
if (signalQuote == null) { return null }
|
||||||
null
|
val attachmentID = (signalQuote.attachments?.firstOrNull() as? DatabaseAttachment)?.attachmentId?.rowId
|
||||||
} else {
|
return Quote(signalQuote.id, signalQuote.author.serialize(), signalQuote.text, attachmentID)
|
||||||
val attachmentID = (signalQuote.attachments?.firstOrNull() as? DatabaseAttachment)?.attachmentId?.rowId
|
|
||||||
Quote(signalQuote.id, signalQuote.author.serialize(), signalQuote.text, attachmentID)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,10 +41,6 @@ class Quote() {
|
|||||||
this.attachmentID = attachmentID
|
this.attachmentID = attachmentID
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isValid(): Boolean {
|
|
||||||
return (timestamp != null && publicKey != null)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toProto(): SignalServiceProtos.DataMessage.Quote? {
|
fun toProto(): SignalServiceProtos.DataMessage.Quote? {
|
||||||
val timestamp = timestamp
|
val timestamp = timestamp
|
||||||
val publicKey = publicKey
|
val publicKey = publicKey
|
||||||
@ -54,7 +51,7 @@ class Quote() {
|
|||||||
val quoteProto = SignalServiceProtos.DataMessage.Quote.newBuilder()
|
val quoteProto = SignalServiceProtos.DataMessage.Quote.newBuilder()
|
||||||
quoteProto.id = timestamp
|
quoteProto.id = timestamp
|
||||||
quoteProto.author = publicKey
|
quoteProto.author = publicKey
|
||||||
text?.let { quoteProto.text = text }
|
text?.let { quoteProto.text = it }
|
||||||
addAttachmentsIfNeeded(quoteProto)
|
addAttachmentsIfNeeded(quoteProto)
|
||||||
// Build
|
// Build
|
||||||
try {
|
try {
|
||||||
@ -66,23 +63,23 @@ class Quote() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun addAttachmentsIfNeeded(quoteProto: SignalServiceProtos.DataMessage.Quote.Builder) {
|
private fun addAttachmentsIfNeeded(quoteProto: SignalServiceProtos.DataMessage.Quote.Builder) {
|
||||||
if (attachmentID == null) return
|
val attachmentID = attachmentID ?: return
|
||||||
val attachment = MessagingModuleConfiguration.shared.messageDataProvider.getSignalAttachmentPointer(attachmentID!!)
|
val database = MessagingModuleConfiguration.shared.messageDataProvider
|
||||||
if (attachment == null) {
|
val pointer = database.getSignalAttachmentPointer(attachmentID)
|
||||||
|
if (pointer == null) {
|
||||||
Log.w(TAG, "Ignoring invalid attachment for quoted message.")
|
Log.w(TAG, "Ignoring invalid attachment for quoted message.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (attachment.url.isNullOrEmpty()) {
|
if (pointer.url.isNullOrEmpty()) {
|
||||||
if (BuildConfig.DEBUG) {
|
if (BuildConfig.DEBUG) {
|
||||||
//TODO equivalent to iOS's preconditionFailure
|
Log.w(TAG,"Sending a message before all associated attachments have been uploaded.")
|
||||||
Log.d(TAG,"Sending a message before all associated attachments have been uploaded.")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val quotedAttachmentProto = SignalServiceProtos.DataMessage.Quote.QuotedAttachment.newBuilder()
|
val quotedAttachmentProto = SignalServiceProtos.DataMessage.Quote.QuotedAttachment.newBuilder()
|
||||||
quotedAttachmentProto.contentType = attachment.contentType
|
quotedAttachmentProto.contentType = pointer.contentType
|
||||||
if (attachment.fileName.isPresent) quotedAttachmentProto.fileName = attachment.fileName.get()
|
if (pointer.fileName.isPresent) { quotedAttachmentProto.fileName = pointer.fileName.get() }
|
||||||
quotedAttachmentProto.thumbnail = Attachment.createAttachmentPointer(attachment)
|
quotedAttachmentProto.thumbnail = Attachment.createAttachmentPointer(pointer)
|
||||||
try {
|
try {
|
||||||
quoteProto.addAttachments(quotedAttachmentProto.build())
|
quoteProto.addAttachments(quotedAttachmentProto.build())
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
@ -12,6 +12,10 @@ import org.session.libsignal.utilities.logging.Log
|
|||||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment as SignalAttachment
|
import org.session.libsession.messaging.sending_receiving.attachments.Attachment as SignalAttachment
|
||||||
|
|
||||||
class VisibleMessage : Message() {
|
class VisibleMessage : Message() {
|
||||||
|
/** In the case of a sync message, the public key of the person the message was targeted at.
|
||||||
|
*
|
||||||
|
* **Note:** `nil` if this isn't a sync message.
|
||||||
|
*/
|
||||||
var syncTarget: String? = null
|
var syncTarget: String? = null
|
||||||
var text: String? = null
|
var text: String? = null
|
||||||
val attachmentIDs: MutableList<Long> = mutableListOf()
|
val attachmentIDs: MutableList<Long> = mutableListOf()
|
||||||
@ -21,46 +25,7 @@ class VisibleMessage : Message() {
|
|||||||
|
|
||||||
override val isSelfSendValid: Boolean = true
|
override val isSelfSendValid: Boolean = true
|
||||||
|
|
||||||
companion object {
|
// region Validation
|
||||||
const val TAG = "VisibleMessage"
|
|
||||||
|
|
||||||
fun fromProto(proto: SignalServiceProtos.Content): VisibleMessage? {
|
|
||||||
val dataMessage = if (proto.hasDataMessage()) proto.dataMessage else return null
|
|
||||||
val result = VisibleMessage()
|
|
||||||
if (dataMessage.hasSyncTarget()) {
|
|
||||||
result.syncTarget = dataMessage.syncTarget
|
|
||||||
}
|
|
||||||
result.text = dataMessage.body
|
|
||||||
// Attachments are handled in MessageReceiver
|
|
||||||
val quoteProto = if (dataMessage.hasQuote()) dataMessage.quote else null
|
|
||||||
quoteProto?.let {
|
|
||||||
val quote = Quote.fromProto(quoteProto)
|
|
||||||
quote?.let { result.quote = quote }
|
|
||||||
}
|
|
||||||
val linkPreviewProto = dataMessage.previewList.firstOrNull()
|
|
||||||
linkPreviewProto?.let {
|
|
||||||
val linkPreview = LinkPreview.fromProto(linkPreviewProto)
|
|
||||||
linkPreview?.let { result.linkPreview = linkPreview }
|
|
||||||
}
|
|
||||||
// TODO Contact
|
|
||||||
val profile = Profile.fromProto(dataMessage)
|
|
||||||
profile?.let { result.profile = profile }
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addSignalAttachments(signalAttachments: List<SignalAttachment>) {
|
|
||||||
val attachmentIDs = signalAttachments.map {
|
|
||||||
val databaseAttachment = it as DatabaseAttachment
|
|
||||||
databaseAttachment.attachmentId.rowId
|
|
||||||
}
|
|
||||||
this.attachmentIDs.addAll(attachmentIDs)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isMediaMessage(): Boolean {
|
|
||||||
return attachmentIDs.isNotEmpty() || quote != null || linkPreview != null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isValid(): Boolean {
|
override fun isValid(): Boolean {
|
||||||
if (!super.isValid()) return false
|
if (!super.isValid()) return false
|
||||||
if (attachmentIDs.isNotEmpty()) return true
|
if (attachmentIDs.isNotEmpty()) return true
|
||||||
@ -68,56 +33,84 @@ class VisibleMessage : Message() {
|
|||||||
if (text.isNotEmpty()) return true
|
if (text.isNotEmpty()) return true
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region Proto Conversion
|
||||||
|
companion object {
|
||||||
|
const val TAG = "VisibleMessage"
|
||||||
|
|
||||||
|
fun fromProto(proto: SignalServiceProtos.Content): VisibleMessage? {
|
||||||
|
val dataMessage = proto.dataMessage ?: return null
|
||||||
|
val result = VisibleMessage()
|
||||||
|
if (dataMessage.hasSyncTarget()) { result.syncTarget = dataMessage.syncTarget }
|
||||||
|
result.text = dataMessage.body
|
||||||
|
// Attachments are handled in MessageReceiver
|
||||||
|
val quoteProto = if (dataMessage.hasQuote()) dataMessage.quote else null
|
||||||
|
if (quoteProto != null) {
|
||||||
|
val quote = Quote.fromProto(quoteProto)
|
||||||
|
result.quote = quote
|
||||||
|
}
|
||||||
|
val linkPreviewProto = dataMessage.previewList.firstOrNull()
|
||||||
|
if (linkPreviewProto != null) {
|
||||||
|
val linkPreview = LinkPreview.fromProto(linkPreviewProto)
|
||||||
|
result.linkPreview = linkPreview
|
||||||
|
}
|
||||||
|
// TODO: Contact
|
||||||
|
val profile = Profile.fromProto(dataMessage)
|
||||||
|
if (profile != null) { result.profile = profile }
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun toProto(): SignalServiceProtos.Content? {
|
override fun toProto(): SignalServiceProtos.Content? {
|
||||||
val proto = SignalServiceProtos.Content.newBuilder()
|
val proto = SignalServiceProtos.Content.newBuilder()
|
||||||
val dataMessage: SignalServiceProtos.DataMessage.Builder
|
val dataMessage: SignalServiceProtos.DataMessage.Builder
|
||||||
// Profile
|
// Profile
|
||||||
val profile = profile
|
val profileProto = profile?.let { it.toProto() }
|
||||||
val profileProto = profile?.toProto()
|
|
||||||
if (profileProto != null) {
|
if (profileProto != null) {
|
||||||
dataMessage = profileProto.toBuilder()
|
dataMessage = profileProto.toBuilder()
|
||||||
} else {
|
} else {
|
||||||
dataMessage = SignalServiceProtos.DataMessage.newBuilder()
|
dataMessage = SignalServiceProtos.DataMessage.newBuilder()
|
||||||
}
|
}
|
||||||
// Text
|
// Text
|
||||||
text?.let { dataMessage.body = text }
|
if (text != null) { dataMessage.body = text }
|
||||||
// Quote
|
// Quote
|
||||||
quote?.let {
|
val quoteProto = quote?.let { it.toProto() }
|
||||||
val quoteProto = it.toProto()
|
if (quoteProto != null) {
|
||||||
if (quoteProto != null) dataMessage.quote = quoteProto
|
dataMessage.quote = quoteProto
|
||||||
}
|
}
|
||||||
//Link preview
|
// Link preview
|
||||||
linkPreview?.let {
|
val linkPreviewProto = linkPreview?.let { it.toProto() }
|
||||||
val linkPreviewProto = it.toProto()
|
if (linkPreviewProto != null) {
|
||||||
linkPreviewProto?.let {
|
dataMessage.addAllPreview(listOf(linkPreviewProto))
|
||||||
dataMessage.addAllPreview(listOf(linkPreviewProto))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
//Attachments
|
// Attachments
|
||||||
val attachments = attachmentIDs.mapNotNull { MessagingModuleConfiguration.shared.messageDataProvider.getSignalAttachmentPointer(it) }
|
val database = MessagingModuleConfiguration.shared.messageDataProvider
|
||||||
if (!attachments.all { !it.url.isNullOrEmpty() }) {
|
val attachments = attachmentIDs.mapNotNull { database.getSignalAttachmentPointer(it) }
|
||||||
|
if (attachments.any { it.url.isNullOrEmpty() }) {
|
||||||
if (BuildConfig.DEBUG) {
|
if (BuildConfig.DEBUG) {
|
||||||
//TODO equivalent to iOS's preconditionFailure
|
Log.w(TAG, "Sending a message before all associated attachments have been uploaded.")
|
||||||
Log.d(TAG, "Sending a message before all associated attachments have been uploaded.")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val attachmentPointers = attachments.mapNotNull { Attachment.createAttachmentPointer(it) }
|
val pointers = attachments.mapNotNull { Attachment.createAttachmentPointer(it) }
|
||||||
dataMessage.addAllAttachments(attachmentPointers)
|
dataMessage.addAllAttachments(pointers)
|
||||||
// TODO Contact
|
// TODO: Contact
|
||||||
// Expiration timer
|
// Expiration timer
|
||||||
// TODO: We * want * expiration timer updates to be explicit. But currently Android will disable the expiration timer for a conversation
|
// TODO: We * want * expiration timer updates to be explicit. But currently Android will disable the expiration timer for a conversation
|
||||||
// if it receives a message without the current expiration timer value attached to it...
|
// if it receives a message without the current expiration timer value attached to it...
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
val context = MessagingModuleConfiguration.shared.context
|
val context = MessagingModuleConfiguration.shared.context
|
||||||
val expiration = if (storage.isClosedGroup(recipient!!)) Recipient.from(context, Address.fromSerialized(GroupUtil.doubleEncodeGroupID(recipient!!)), false).expireMessages
|
val expiration = if (storage.isClosedGroup(recipient!!)) {
|
||||||
else Recipient.from(context, Address.fromSerialized(recipient!!), false).expireMessages
|
Recipient.from(context, Address.fromSerialized(GroupUtil.doubleEncodeGroupID(recipient!!)), false).expireMessages
|
||||||
|
} else {
|
||||||
|
Recipient.from(context, Address.fromSerialized(recipient!!), false).expireMessages
|
||||||
|
}
|
||||||
dataMessage.expireTimer = expiration
|
dataMessage.expireTimer = expiration
|
||||||
// Group context
|
// Group context
|
||||||
if (storage.isClosedGroup(recipient!!)) {
|
if (storage.isClosedGroup(recipient!!)) {
|
||||||
try {
|
try {
|
||||||
setGroupContext(dataMessage)
|
setGroupContext(dataMessage)
|
||||||
} catch(e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(TAG, "Couldn't construct visible message proto from: $this")
|
Log.w(TAG, "Couldn't construct visible message proto from: $this")
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@ -135,4 +128,17 @@ class VisibleMessage : Message() {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
fun addSignalAttachments(signalAttachments: List<SignalAttachment>) {
|
||||||
|
val attachmentIDs = signalAttachments.map {
|
||||||
|
val databaseAttachment = it as DatabaseAttachment
|
||||||
|
databaseAttachment.attachmentId.rowId
|
||||||
|
}
|
||||||
|
this.attachmentIDs.addAll(attachmentIDs)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isMediaMessage(): Boolean {
|
||||||
|
return attachmentIDs.isNotEmpty() || quote != null || linkPreview != null
|
||||||
|
}
|
||||||
}
|
}
|
@ -5,7 +5,6 @@ import com.fasterxml.jackson.databind.annotation.JsonNaming
|
|||||||
import com.fasterxml.jackson.databind.type.TypeFactory
|
import com.fasterxml.jackson.databind.type.TypeFactory
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import nl.komponents.kovenant.Kovenant
|
|
||||||
import nl.komponents.kovenant.Promise
|
import nl.komponents.kovenant.Promise
|
||||||
import nl.komponents.kovenant.functional.bind
|
import nl.komponents.kovenant.functional.bind
|
||||||
import nl.komponents.kovenant.functional.map
|
import nl.komponents.kovenant.functional.map
|
||||||
@ -14,7 +13,6 @@ import okhttp3.HttpUrl
|
|||||||
import okhttp3.MediaType
|
import okhttp3.MediaType
|
||||||
import okhttp3.RequestBody
|
import okhttp3.RequestBody
|
||||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2.Error
|
|
||||||
import org.session.libsession.snode.OnionRequestAPI
|
import org.session.libsession.snode.OnionRequestAPI
|
||||||
import org.session.libsession.utilities.AESGCM
|
import org.session.libsession.utilities.AESGCM
|
||||||
import org.session.libsignal.service.loki.HTTP
|
import org.session.libsignal.service.loki.HTTP
|
||||||
@ -29,108 +27,83 @@ import org.whispersystems.curve25519.Curve25519
|
|||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
object OpenGroupAPIV2 {
|
object OpenGroupAPIV2 {
|
||||||
|
|
||||||
private val moderators: HashMap<String, Set<String>> = hashMapOf() // Server URL to (channel ID to set of moderator IDs)
|
private val moderators: HashMap<String, Set<String>> = hashMapOf() // Server URL to (channel ID to set of moderator IDs)
|
||||||
const val DEFAULT_SERVER = "http://116.203.70.33"
|
private val curve = Curve25519.getInstance(Curve25519.BEST)
|
||||||
private const val DEFAULT_SERVER_PUBLIC_KEY = "a03c383cf63c3c4efe67acc52112a6dd734b3a946b9545f488aaa93da7991238"
|
|
||||||
|
|
||||||
val defaultRooms = MutableSharedFlow<List<DefaultGroup>>(replay = 1)
|
val defaultRooms = MutableSharedFlow<List<DefaultGroup>>(replay = 1)
|
||||||
|
|
||||||
private val curve = Curve25519.getInstance(Curve25519.BEST)
|
private const val DEFAULT_SERVER_PUBLIC_KEY = "a03c383cf63c3c4efe67acc52112a6dd734b3a946b9545f488aaa93da7991238"
|
||||||
|
const val DEFAULT_SERVER = "http://116.203.70.33"
|
||||||
sealed class Error : Exception() {
|
|
||||||
object GENERIC : Error()
|
|
||||||
object PARSING_FAILED : Error()
|
|
||||||
object DECRYPTION_FAILED : Error()
|
|
||||||
object SIGNING_FAILED : Error()
|
|
||||||
object INVALID_URL : Error()
|
|
||||||
object NO_PUBLIC_KEY : Error()
|
|
||||||
|
|
||||||
fun errorDescription() = when (this) {
|
|
||||||
Error.GENERIC -> "An error occurred."
|
|
||||||
Error.PARSING_FAILED -> "Invalid response."
|
|
||||||
Error.DECRYPTION_FAILED -> "Couldn't decrypt response."
|
|
||||||
Error.SIGNING_FAILED -> "Couldn't sign message."
|
|
||||||
Error.INVALID_URL -> "Invalid URL."
|
|
||||||
Error.NO_PUBLIC_KEY -> "Couldn't find server public key."
|
|
||||||
}
|
|
||||||
|
|
||||||
|
sealed class Error(message: String) : Exception(message) {
|
||||||
|
object Generic : Error("An error occurred.")
|
||||||
|
object ParsingFailed : Error("Invalid response.")
|
||||||
|
object DecryptionFailed : Error("Couldn't decrypt response.")
|
||||||
|
object SigningFailed : Error("Couldn't sign message.")
|
||||||
|
object InvalidURL : Error("Invalid URL.")
|
||||||
|
object NoPublicKey : Error("Couldn't find server public key.")
|
||||||
}
|
}
|
||||||
|
|
||||||
data class DefaultGroup(val id: String,
|
data class DefaultGroup(val id: String, val name: String, val image: ByteArray?) {
|
||||||
val name: String,
|
|
||||||
val image: ByteArray?) {
|
val joinURL: String get() = "$DEFAULT_SERVER/$id?public_key=$DEFAULT_SERVER_PUBLIC_KEY"
|
||||||
fun toJoinUrl(): String = "$DEFAULT_SERVER/$id?public_key=$DEFAULT_SERVER_PUBLIC_KEY"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Info(
|
data class Info(val id: String, val name: String, val imageID: String?)
|
||||||
val id: String,
|
|
||||||
val name: String,
|
|
||||||
val imageID: String?
|
|
||||||
)
|
|
||||||
|
|
||||||
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy::class)
|
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy::class)
|
||||||
data class CompactPollRequest(val roomId: String,
|
data class CompactPollRequest(val roomID: String, val authToken: String, val fromDeletionServerID: Long?, val fromMessageServerID: Long?)
|
||||||
val authToken: String,
|
data class CompactPollResult(val messages: List<OpenGroupMessageV2>, val deletions: List<Long>, val moderators: List<String>)
|
||||||
val fromDeletionServerId: Long?,
|
|
||||||
val fromMessageServerId: Long?
|
|
||||||
)
|
|
||||||
|
|
||||||
data class CompactPollResult(val messages: List<OpenGroupMessageV2>,
|
|
||||||
val deletions: List<Long>,
|
|
||||||
val moderators: List<String>
|
|
||||||
)
|
|
||||||
|
|
||||||
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy::class)
|
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy::class)
|
||||||
data class MessageDeletion @JvmOverloads constructor(val id: Long = 0,
|
data class MessageDeletion
|
||||||
val deletedMessageId: Long = 0
|
@JvmOverloads constructor(val id: Long = 0, val deletedMessageId: Long = 0
|
||||||
) {
|
) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val EMPTY = MessageDeletion()
|
val EMPTY = MessageDeletion()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Request(
|
data class Request(
|
||||||
val verb: HTTP.Verb,
|
val verb: HTTP.Verb,
|
||||||
val room: String?,
|
val room: String?,
|
||||||
val server: String,
|
val server: String,
|
||||||
val endpoint: String,
|
val endpoint: String,
|
||||||
val queryParameters: Map<String, String> = mapOf(),
|
val queryParameters: Map<String, String> = mapOf(),
|
||||||
val parameters: Any? = null,
|
val parameters: Any? = null,
|
||||||
val headers: Map<String, String> = mapOf(),
|
val headers: Map<String, String> = mapOf(),
|
||||||
val isAuthRequired: Boolean = true,
|
val isAuthRequired: Boolean = true,
|
||||||
// Always `true` under normal circumstances. You might want to disable
|
/**
|
||||||
// this when running over Lokinet.
|
* Always `true` under normal circumstances. You might want to disable
|
||||||
val useOnionRouting: Boolean = true
|
* this when running over Lokinet.
|
||||||
|
*/
|
||||||
|
val useOnionRouting: Boolean = true
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun createBody(parameters: Any?): RequestBody? {
|
private fun createBody(parameters: Any?): RequestBody? {
|
||||||
if (parameters == null) return null
|
if (parameters == null) return null
|
||||||
|
|
||||||
val parametersAsJSON = JsonUtil.toJson(parameters)
|
val parametersAsJSON = JsonUtil.toJson(parameters)
|
||||||
return RequestBody.create(MediaType.get("application/json"), parametersAsJSON)
|
return RequestBody.create(MediaType.get("application/json"), parametersAsJSON)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun send(request: Request, isJsonRequired: Boolean = true): Promise<Map<*, *>, Exception> {
|
private fun send(request: Request, isJsonRequired: Boolean = true): Promise<Map<*, *>, Exception> {
|
||||||
val parsed = HttpUrl.parse(request.server) ?: return Promise.ofFail(Error.INVALID_URL)
|
val url = HttpUrl.parse(request.server) ?: return Promise.ofFail(Error.InvalidURL)
|
||||||
val urlBuilder = HttpUrl.Builder()
|
val urlBuilder = HttpUrl.Builder()
|
||||||
.scheme(parsed.scheme())
|
.scheme(url.scheme())
|
||||||
.host(parsed.host())
|
.host(url.host())
|
||||||
.port(parsed.port())
|
.port(url.port())
|
||||||
.addPathSegments(request.endpoint)
|
.addPathSegments(request.endpoint)
|
||||||
|
|
||||||
if (request.verb == GET) {
|
if (request.verb == GET) {
|
||||||
for ((key, value) in request.queryParameters) {
|
for ((key, value) in request.queryParameters) {
|
||||||
urlBuilder.addQueryParameter(key, value)
|
urlBuilder.addQueryParameter(key, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun execute(token: String?): Promise<Map<*, *>, Exception> {
|
fun execute(token: String?): Promise<Map<*, *>, Exception> {
|
||||||
val requestBuilder = okhttp3.Request.Builder()
|
val requestBuilder = okhttp3.Request.Builder()
|
||||||
.url(urlBuilder.build())
|
.url(urlBuilder.build())
|
||||||
.headers(Headers.of(request.headers))
|
.headers(Headers.of(request.headers))
|
||||||
if (request.isAuthRequired) {
|
if (request.isAuthRequired) {
|
||||||
if (token.isNullOrEmpty()) throw IllegalStateException("No auth token for request")
|
if (token.isNullOrEmpty()) throw IllegalStateException("No auth token for request.")
|
||||||
requestBuilder.header("Authorization", token)
|
requestBuilder.header("Authorization", token)
|
||||||
}
|
}
|
||||||
when (request.verb) {
|
when (request.verb) {
|
||||||
@ -139,25 +112,25 @@ object OpenGroupAPIV2 {
|
|||||||
POST -> requestBuilder.post(createBody(request.parameters)!!)
|
POST -> requestBuilder.post(createBody(request.parameters)!!)
|
||||||
DELETE -> requestBuilder.delete(createBody(request.parameters))
|
DELETE -> requestBuilder.delete(createBody(request.parameters))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!request.room.isNullOrEmpty()) {
|
if (!request.room.isNullOrEmpty()) {
|
||||||
requestBuilder.header("Room", request.room)
|
requestBuilder.header("Room", request.room)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.useOnionRouting) {
|
if (request.useOnionRouting) {
|
||||||
val publicKey = MessagingModuleConfiguration.shared.storage.getOpenGroupPublicKey(request.server)
|
val publicKey = MessagingModuleConfiguration.shared.storage.getOpenGroupPublicKey(request.server)
|
||||||
?: return Promise.ofFail(Error.NO_PUBLIC_KEY)
|
?: return Promise.ofFail(Error.NoPublicKey)
|
||||||
return OnionRequestAPI.sendOnionRequest(requestBuilder.build(), request.server, publicKey, isJSONRequired = isJsonRequired)
|
return OnionRequestAPI.sendOnionRequest(requestBuilder.build(), request.server, publicKey, isJSONRequired = isJsonRequired).fail { e ->
|
||||||
.fail { e ->
|
// A 401 means that we didn't provide a (valid) auth token for a route that required one. We use this as an
|
||||||
if (e is OnionRequestAPI.HTTPRequestFailedAtDestinationException && e.statusCode == 401) {
|
// indication that the token we're using has expired. Note that a 403 has a different meaning; it means that
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
// we provided a valid token but it doesn't have a high enough permission level for the route in question.
|
||||||
if (request.room != null) {
|
if (e is OnionRequestAPI.HTTPRequestFailedAtDestinationException && e.statusCode == 401) {
|
||||||
storage.removeAuthToken("${request.server}.${request.room}")
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
} else {
|
if (request.room != null) {
|
||||||
storage.removeAuthToken(request.server)
|
storage.removeAuthToken("${request.server}.${request.room}")
|
||||||
}
|
} else {
|
||||||
}
|
storage.removeAuthToken(request.server)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return Promise.ofFail(IllegalStateException("It's currently not allowed to send non onion routed requests."))
|
return Promise.ofFail(IllegalStateException("It's currently not allowed to send non onion routed requests."))
|
||||||
}
|
}
|
||||||
@ -172,52 +145,51 @@ object OpenGroupAPIV2 {
|
|||||||
fun downloadOpenGroupProfilePicture(roomID: String, server: String): Promise<ByteArray, Exception> {
|
fun downloadOpenGroupProfilePicture(roomID: String, server: String): Promise<ByteArray, Exception> {
|
||||||
val request = Request(verb = GET, room = roomID, server = server, endpoint = "rooms/$roomID/image", isAuthRequired = false)
|
val request = Request(verb = GET, room = roomID, server = server, endpoint = "rooms/$roomID/image", isAuthRequired = false)
|
||||||
return send(request).map { json ->
|
return send(request).map { json ->
|
||||||
val result = json["result"] as? String ?: throw Error.PARSING_FAILED
|
val result = json["result"] as? String ?: throw Error.ParsingFailed
|
||||||
decode(result)
|
decode(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// region Authorization
|
||||||
fun getAuthToken(room: String, server: String): Promise<String, Exception> {
|
fun getAuthToken(room: String, server: String): Promise<String, Exception> {
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
return storage.getAuthToken(room, server)?.let {
|
return storage.getAuthToken(room, server)?.let {
|
||||||
Promise.of(it)
|
Promise.of(it)
|
||||||
} ?: run {
|
} ?: run {
|
||||||
requestNewAuthToken(room, server)
|
requestNewAuthToken(room, server)
|
||||||
.bind { claimAuthToken(it, room, server) }
|
.bind { claimAuthToken(it, room, server) }
|
||||||
.success { authToken ->
|
.success { authToken ->
|
||||||
storage.setAuthToken(room, server, authToken)
|
storage.setAuthToken(room, server, authToken)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun requestNewAuthToken(room: String, server: String): Promise<String, Exception> {
|
fun requestNewAuthToken(room: String, server: String): Promise<String, Exception> {
|
||||||
val (publicKey, privateKey) = MessagingModuleConfiguration.shared.storage.getUserKeyPair()
|
val (publicKey, privateKey) = MessagingModuleConfiguration.shared.storage.getUserKeyPair()
|
||||||
?: return Promise.ofFail(Error.GENERIC)
|
?: return Promise.ofFail(Error.Generic)
|
||||||
val queryParameters = mutableMapOf("public_key" to publicKey)
|
val queryParameters = mutableMapOf( "public_key" to publicKey )
|
||||||
val request = Request(GET, room, server, "auth_token_challenge", queryParameters, isAuthRequired = false, parameters = null)
|
val request = Request(GET, room, server, "auth_token_challenge", queryParameters, isAuthRequired = false, parameters = null)
|
||||||
return send(request).map { json ->
|
return send(request).map { json ->
|
||||||
val challenge = json["challenge"] as? Map<*, *> ?: throw Error.PARSING_FAILED
|
val challenge = json["challenge"] as? Map<*, *> ?: throw Error.ParsingFailed
|
||||||
val base64EncodedCiphertext = challenge["ciphertext"] as? String
|
val base64EncodedCiphertext = challenge["ciphertext"] as? String ?: throw Error.ParsingFailed
|
||||||
?: throw Error.PARSING_FAILED
|
val base64EncodedEphemeralPublicKey = challenge["ephemeral_public_key"] as? String ?: throw Error.ParsingFailed
|
||||||
val base64EncodedEphemeralPublicKey = challenge["ephemeral_public_key"] as? String
|
|
||||||
?: throw Error.PARSING_FAILED
|
|
||||||
val ciphertext = decode(base64EncodedCiphertext)
|
val ciphertext = decode(base64EncodedCiphertext)
|
||||||
val ephemeralPublicKey = decode(base64EncodedEphemeralPublicKey)
|
val ephemeralPublicKey = decode(base64EncodedEphemeralPublicKey)
|
||||||
val symmetricKey = AESGCM.generateSymmetricKey(ephemeralPublicKey, privateKey)
|
val symmetricKey = AESGCM.generateSymmetricKey(ephemeralPublicKey, privateKey)
|
||||||
val tokenAsData = try {
|
val tokenAsData = try {
|
||||||
AESGCM.decrypt(ciphertext, symmetricKey)
|
AESGCM.decrypt(ciphertext, symmetricKey)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw Error.DECRYPTION_FAILED
|
throw Error.DecryptionFailed
|
||||||
}
|
}
|
||||||
tokenAsData.toHexString()
|
tokenAsData.toHexString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun claimAuthToken(authToken: String, room: String, server: String): Promise<String, Exception> {
|
fun claimAuthToken(authToken: String, room: String, server: String): Promise<String, Exception> {
|
||||||
val parameters = mapOf("public_key" to MessagingModuleConfiguration.shared.storage.getUserPublicKey()!!)
|
val parameters = mapOf( "public_key" to MessagingModuleConfiguration.shared.storage.getUserPublicKey()!! )
|
||||||
val headers = mapOf("Authorization" to authToken)
|
val headers = mapOf( "Authorization" to authToken )
|
||||||
val request = Request(verb = POST, room = room, server = server, endpoint = "claim_auth_token",
|
val request = Request(verb = POST, room = room, server = server, endpoint = "claim_auth_token",
|
||||||
parameters = parameters, headers = headers, isAuthRequired = false)
|
parameters = parameters, headers = headers, isAuthRequired = false)
|
||||||
return send(request).map { authToken }
|
return send(request).map { authToken }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -227,33 +199,36 @@ object OpenGroupAPIV2 {
|
|||||||
MessagingModuleConfiguration.shared.storage.removeAuthToken(room, server)
|
MessagingModuleConfiguration.shared.storage.removeAuthToken(room, server)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// endregion
|
||||||
|
|
||||||
// region Sending
|
// region Upload/Download
|
||||||
fun upload(file: ByteArray, room: String, server: String): Promise<Long, Exception> {
|
fun upload(file: ByteArray, room: String, server: String): Promise<Long, Exception> {
|
||||||
val base64EncodedFile = encodeBytes(file)
|
val base64EncodedFile = encodeBytes(file)
|
||||||
val parameters = mapOf("file" to base64EncodedFile)
|
val parameters = mapOf( "file" to base64EncodedFile )
|
||||||
val request = Request(verb = POST, room = room, server = server, endpoint = "files", parameters = parameters)
|
val request = Request(verb = POST, room = room, server = server, endpoint = "files", parameters = parameters)
|
||||||
return send(request).map { json ->
|
return send(request).map { json ->
|
||||||
json["result"] as? Long ?: throw Error.PARSING_FAILED
|
json["result"] as? Long ?: throw Error.ParsingFailed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun download(file: Long, room: String, server: String): Promise<ByteArray, Exception> {
|
fun download(file: Long, room: String, server: String): Promise<ByteArray, Exception> {
|
||||||
val request = Request(verb = GET, room = room, server = server, endpoint = "files/$file")
|
val request = Request(verb = GET, room = room, server = server, endpoint = "files/$file")
|
||||||
return send(request).map { json ->
|
return send(request).map { json ->
|
||||||
val base64EncodedFile = json["result"] as? String ?: throw Error.PARSING_FAILED
|
val base64EncodedFile = json["result"] as? String ?: throw Error.ParsingFailed
|
||||||
decode(base64EncodedFile) ?: throw Error.PARSING_FAILED
|
decode(base64EncodedFile) ?: throw Error.ParsingFailed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region Sending
|
||||||
fun send(message: OpenGroupMessageV2, room: String, server: String): Promise<OpenGroupMessageV2, Exception> {
|
fun send(message: OpenGroupMessageV2, room: String, server: String): Promise<OpenGroupMessageV2, Exception> {
|
||||||
val signedMessage = message.sign() ?: return Promise.ofFail(Error.SIGNING_FAILED)
|
val signedMessage = message.sign() ?: return Promise.ofFail(Error.SigningFailed)
|
||||||
val jsonMessage = signedMessage.toJSON()
|
val jsonMessage = signedMessage.toJSON()
|
||||||
val request = Request(verb = POST, room = room, server = server, endpoint = "messages", parameters = jsonMessage)
|
val request = Request(verb = POST, room = room, server = server, endpoint = "messages", parameters = jsonMessage)
|
||||||
return send(request).map { json ->
|
return send(request).map { json ->
|
||||||
@Suppress("UNCHECKED_CAST") val rawMessage = json["message"] as? Map<String, Any>
|
@Suppress("UNCHECKED_CAST") val rawMessage = json["message"] as? Map<String, Any>
|
||||||
?: throw Error.PARSING_FAILED
|
?: throw Error.ParsingFailed
|
||||||
OpenGroupMessageV2.fromJSON(rawMessage) ?: throw Error.PARSING_FAILED
|
OpenGroupMessageV2.fromJSON(rawMessage) ?: throw Error.ParsingFailed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
@ -266,37 +241,42 @@ object OpenGroupAPIV2 {
|
|||||||
queryParameters += "from_server_id" to lastId.toString()
|
queryParameters += "from_server_id" to lastId.toString()
|
||||||
}
|
}
|
||||||
val request = Request(verb = GET, room = room, server = server, endpoint = "messages", queryParameters = queryParameters)
|
val request = Request(verb = GET, room = room, server = server, endpoint = "messages", queryParameters = queryParameters)
|
||||||
return send(request).map { jsonList ->
|
return send(request).map { json ->
|
||||||
@Suppress("UNCHECKED_CAST") val rawMessages = jsonList["messages"] as? List<Map<String, Any>>
|
@Suppress("UNCHECKED_CAST") val rawMessages = json["messages"] as? List<Map<String, Any>>
|
||||||
?: throw Error.PARSING_FAILED
|
?: throw Error.ParsingFailed
|
||||||
val lastMessageServerId = storage.getLastMessageServerId(room, server) ?: 0
|
parseMessages(room, server, rawMessages)
|
||||||
|
|
||||||
var currentMax = lastMessageServerId
|
|
||||||
val messages = rawMessages.mapNotNull { json ->
|
|
||||||
try {
|
|
||||||
val message = OpenGroupMessageV2.fromJSON(json) ?: return@mapNotNull null
|
|
||||||
if (message.serverID == null || message.sender.isNullOrEmpty()) return@mapNotNull null
|
|
||||||
val sender = message.sender
|
|
||||||
val data = decode(message.base64EncodedData)
|
|
||||||
val signature = decode(message.base64EncodedSignature)
|
|
||||||
val publicKey = Hex.fromStringCondensed(sender.removing05PrefixIfNeeded())
|
|
||||||
val isValid = curve.verifySignature(publicKey, data, signature)
|
|
||||||
if (!isValid) {
|
|
||||||
Log.d("Loki", "Ignoring message with invalid signature")
|
|
||||||
return@mapNotNull null
|
|
||||||
}
|
|
||||||
if (message.serverID > lastMessageServerId) {
|
|
||||||
currentMax = message.serverID
|
|
||||||
}
|
|
||||||
message
|
|
||||||
} catch (e: Exception) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
storage.setLastMessageServerId(room, server, currentMax)
|
|
||||||
messages
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun parseMessages(room: String, server: String, rawMessages: List<Map<*, *>>): List<OpenGroupMessageV2> {
|
||||||
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
|
val lastMessageServerID = storage.getLastMessageServerId(room, server) ?: 0
|
||||||
|
var currentLastMessageServerID = lastMessageServerID
|
||||||
|
val messages = rawMessages.mapNotNull { json ->
|
||||||
|
json as Map<String, Any>
|
||||||
|
try {
|
||||||
|
val message = OpenGroupMessageV2.fromJSON(json) ?: return@mapNotNull null
|
||||||
|
if (message.serverID == null || message.sender.isNullOrEmpty()) return@mapNotNull null
|
||||||
|
val sender = message.sender
|
||||||
|
val data = decode(message.base64EncodedData)
|
||||||
|
val signature = decode(message.base64EncodedSignature)
|
||||||
|
val publicKey = Hex.fromStringCondensed(sender.removing05PrefixIfNeeded())
|
||||||
|
val isValid = curve.verifySignature(publicKey, data, signature)
|
||||||
|
if (!isValid) {
|
||||||
|
Log.d("Loki", "Ignoring message with invalid signature.")
|
||||||
|
return@mapNotNull null
|
||||||
|
}
|
||||||
|
if (message.serverID > lastMessageServerID) {
|
||||||
|
currentLastMessageServerID = message.serverID
|
||||||
|
}
|
||||||
|
message
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
storage.setLastMessageServerId(room, server, currentLastMessageServerID)
|
||||||
|
return messages
|
||||||
|
}
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
// region Message Deletion
|
// region Message Deletion
|
||||||
@ -304,7 +284,7 @@ object OpenGroupAPIV2 {
|
|||||||
fun deleteMessage(serverID: Long, room: String, server: String): Promise<Unit, Exception> {
|
fun deleteMessage(serverID: Long, room: String, server: String): Promise<Unit, Exception> {
|
||||||
val request = Request(verb = DELETE, room = room, server = server, endpoint = "messages/$serverID")
|
val request = Request(verb = DELETE, room = room, server = server, endpoint = "messages/$serverID")
|
||||||
return send(request).map {
|
return send(request).map {
|
||||||
Log.d("Loki", "Deleted server message")
|
Log.d("Loki", "Message deletion successful.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -318,7 +298,7 @@ object OpenGroupAPIV2 {
|
|||||||
return send(request).map { json ->
|
return send(request).map { json ->
|
||||||
val type = TypeFactory.defaultInstance().constructCollectionType(List::class.java, MessageDeletion::class.java)
|
val type = TypeFactory.defaultInstance().constructCollectionType(List::class.java, MessageDeletion::class.java)
|
||||||
val idsAsString = JsonUtil.toJson(json["ids"])
|
val idsAsString = JsonUtil.toJson(json["ids"])
|
||||||
val serverIDs = JsonUtil.fromJson<List<MessageDeletion>>(idsAsString, type) ?: throw Error.PARSING_FAILED
|
val serverIDs = JsonUtil.fromJson<List<MessageDeletion>>(idsAsString, type) ?: throw Error.ParsingFailed
|
||||||
val lastMessageServerId = storage.getLastDeletionServerId(room, server) ?: 0
|
val lastMessageServerId = storage.getLastDeletionServerId(room, server) ?: 0
|
||||||
val serverID = serverIDs.maxByOrNull {it.id } ?: MessageDeletion.EMPTY
|
val serverID = serverIDs.maxByOrNull {it.id } ?: MessageDeletion.EMPTY
|
||||||
if (serverID.id > lastMessageServerId) {
|
if (serverID.id > lastMessageServerId) {
|
||||||
@ -338,7 +318,7 @@ object OpenGroupAPIV2 {
|
|||||||
val request = Request(verb = GET, room = room, server = server, endpoint = "moderators")
|
val request = Request(verb = GET, room = room, server = server, endpoint = "moderators")
|
||||||
return send(request).map { json ->
|
return send(request).map { json ->
|
||||||
@Suppress("UNCHECKED_CAST") val moderatorsJson = json["moderators"] as? List<String>
|
@Suppress("UNCHECKED_CAST") val moderatorsJson = json["moderators"] as? List<String>
|
||||||
?: throw Error.PARSING_FAILED
|
?: throw Error.ParsingFailed
|
||||||
val id = "$server.$room"
|
val id = "$server.$room"
|
||||||
handleModerators(id, moderatorsJson)
|
handleModerators(id, moderatorsJson)
|
||||||
moderatorsJson
|
moderatorsJson
|
||||||
@ -347,90 +327,77 @@ object OpenGroupAPIV2 {
|
|||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun ban(publicKey: String, room: String, server: String): Promise<Unit, Exception> {
|
fun ban(publicKey: String, room: String, server: String): Promise<Unit, Exception> {
|
||||||
val parameters = mapOf("public_key" to publicKey)
|
val parameters = mapOf( "public_key" to publicKey )
|
||||||
val request = Request(verb = POST, room = room, server = server, endpoint = "block_list", parameters = parameters)
|
val request = Request(verb = POST, room = room, server = server, endpoint = "block_list", parameters = parameters)
|
||||||
return send(request).map {
|
return send(request).map {
|
||||||
Log.d("Loki", "Banned user $publicKey from $server.$room")
|
Log.d("Loki", "Banned user: $publicKey from: $server.$room.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun unban(publicKey: String, room: String, server: String): Promise<Unit, Exception> {
|
fun unban(publicKey: String, room: String, server: String): Promise<Unit, Exception> {
|
||||||
val request = Request(verb = DELETE, room = room, server = server, endpoint = "block_list/$publicKey")
|
val request = Request(verb = DELETE, room = room, server = server, endpoint = "block_list/$publicKey")
|
||||||
return send(request).map {
|
return send(request).map {
|
||||||
Log.d("Loki", "Unbanned user $publicKey from $server.$room")
|
Log.d("Loki", "Unbanned user: $publicKey from: $server.$room")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun isUserModerator(publicKey: String, room: String, server: String): Boolean =
|
fun isUserModerator(publicKey: String, room: String, server: String): Boolean =
|
||||||
moderators["$server.$room"]?.contains(publicKey) ?: false
|
moderators["$server.$room"]?.contains(publicKey) ?: false
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
// region General
|
// region General
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
fun getCompactPoll(rooms: List<String>, server: String): Promise<Map<String, CompactPollResult>, Exception> {
|
fun getCompactPoll(rooms: List<String>, server: String): Promise<Map<String, CompactPollResult>, Exception> {
|
||||||
val requestAuths = rooms.associateWith { room -> getAuthToken(room, server) }
|
val authTokenRequests = rooms.associateWith { room -> getAuthToken(room, server) }
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
val requests = rooms.mapNotNull { room ->
|
val requests = rooms.mapNotNull { room ->
|
||||||
val authToken = try {
|
val authToken = try {
|
||||||
requestAuths[room]?.get()
|
authTokenRequests[room]?.get()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("Loki", "Failed to get auth token for $room", e)
|
Log.e("Loki", "Failed to get auth token for $room.", e)
|
||||||
null
|
null
|
||||||
} ?: return@mapNotNull null
|
} ?: return@mapNotNull null
|
||||||
|
CompactPollRequest(
|
||||||
CompactPollRequest(roomId = room,
|
roomID = room,
|
||||||
authToken = authToken,
|
authToken = authToken,
|
||||||
fromDeletionServerId = storage.getLastDeletionServerId(room, server),
|
fromDeletionServerID = storage.getLastDeletionServerId(room, server),
|
||||||
fromMessageServerId = storage.getLastMessageServerId(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))
|
val request = Request(verb = POST, room = null, server = server, endpoint = "compact_poll", isAuthRequired = false, parameters = mapOf( "requests" to requests ))
|
||||||
// build a request for all rooms
|
|
||||||
return send(request = request).map { json ->
|
return send(request = request).map { json ->
|
||||||
val results = json["results"] as? List<*> ?: throw Error.PARSING_FAILED
|
val results = json["results"] as? List<*> ?: throw Error.ParsingFailed
|
||||||
|
results.mapNotNull { json ->
|
||||||
results.mapNotNull { roomJson ->
|
if (json !is Map<*,*>) return@mapNotNull null
|
||||||
if (roomJson !is Map<*,*>) return@mapNotNull null
|
val roomID = json["room_id"] as? String ?: return@mapNotNull null
|
||||||
val roomId = roomJson["room_id"] as? String ?: return@mapNotNull null
|
// A 401 means that we didn't provide a (valid) auth token for a route that required one. We use this as an
|
||||||
|
// indication that the token we're using has expired. Note that a 403 has a different meaning; it means that
|
||||||
// check the status was fine
|
// we provided a valid token but it doesn't have a high enough permission level for the route in question.
|
||||||
val statusCode = roomJson["status_code"] as? Int ?: return@mapNotNull null
|
val statusCode = json["status_code"] as? Int ?: return@mapNotNull null
|
||||||
if (statusCode == 401) {
|
if (statusCode == 401) {
|
||||||
// delete auth token and return null
|
// delete auth token and return null
|
||||||
storage.removeAuthToken(roomId, server)
|
storage.removeAuthToken(roomID, server)
|
||||||
}
|
}
|
||||||
|
// Moderators
|
||||||
// check and store mods
|
val moderators = json["moderators"] as? List<String> ?: return@mapNotNull null
|
||||||
val moderators = roomJson["moderators"] as? List<String> ?: return@mapNotNull null
|
handleModerators("$server.$roomID", moderators)
|
||||||
handleModerators("$server.$roomId", moderators)
|
// Deletions
|
||||||
|
|
||||||
// get deletions
|
|
||||||
val type = TypeFactory.defaultInstance().constructCollectionType(List::class.java, MessageDeletion::class.java)
|
val type = TypeFactory.defaultInstance().constructCollectionType(List::class.java, MessageDeletion::class.java)
|
||||||
val idsAsString = JsonUtil.toJson(roomJson["deletions"])
|
val idsAsString = JsonUtil.toJson(json["deletions"])
|
||||||
val deletedServerIDs = JsonUtil.fromJson<List<MessageDeletion>>(idsAsString, type) ?: throw Error.PARSING_FAILED
|
val deletedServerIDs = JsonUtil.fromJson<List<MessageDeletion>>(idsAsString, type) ?: throw Error.ParsingFailed
|
||||||
val lastDeletionServerId = storage.getLastDeletionServerId(roomId, server) ?: 0
|
val lastDeletionServerID = storage.getLastDeletionServerId(roomID, server) ?: 0
|
||||||
val serverID = deletedServerIDs.maxByOrNull {it.id } ?: MessageDeletion.EMPTY
|
val serverID = deletedServerIDs.maxByOrNull { it.id } ?: MessageDeletion.EMPTY
|
||||||
if (serverID.id > lastDeletionServerId) {
|
if (serverID.id > lastDeletionServerID) {
|
||||||
storage.setLastDeletionServerId(roomId, server, serverID.id)
|
storage.setLastDeletionServerId(roomID, server, serverID.id)
|
||||||
}
|
}
|
||||||
|
// Messages
|
||||||
// get messages
|
val rawMessages = json["messages"] as? List<Map<String, Any>> ?: return@mapNotNull null
|
||||||
val rawMessages = roomJson["messages"] as? List<Map<String, Any>> ?: return@mapNotNull null // parsing failed
|
val messages = parseMessages(roomID, server, rawMessages)
|
||||||
|
roomID to CompactPollResult(
|
||||||
val lastMessageServerId = storage.getLastMessageServerId(roomId, server) ?: 0
|
messages = messages,
|
||||||
var currentMax = lastMessageServerId
|
deletions = deletedServerIDs.map { it.deletedMessageId },
|
||||||
val messages = rawMessages.mapNotNull { rawMessage ->
|
moderators = moderators
|
||||||
val message = OpenGroupMessageV2.fromJSON(rawMessage)?.apply {
|
|
||||||
currentMax = maxOf(currentMax,this.serverID ?: 0)
|
|
||||||
}
|
|
||||||
message
|
|
||||||
}
|
|
||||||
storage.setLastMessageServerId(roomId, server, currentMax)
|
|
||||||
roomId to CompactPollResult(
|
|
||||||
messages = messages,
|
|
||||||
deletions = deletedServerIDs.map { it.deletedMessageId },
|
|
||||||
moderators = moderators
|
|
||||||
)
|
)
|
||||||
}.toMap()
|
}.toMap()
|
||||||
}
|
}
|
||||||
@ -443,7 +410,7 @@ object OpenGroupAPIV2 {
|
|||||||
val earlyGroups = groups.map { group ->
|
val earlyGroups = groups.map { group ->
|
||||||
DefaultGroup(group.id, group.name, null)
|
DefaultGroup(group.id, group.name, null)
|
||||||
}
|
}
|
||||||
// see if we have any cached rooms, and if they already have images, don't overwrite with early non-image results
|
// See if we have any cached rooms, and if they already have images don't overwrite them with early non-image results
|
||||||
defaultRooms.replayCache.firstOrNull()?.let { replayed ->
|
defaultRooms.replayCache.firstOrNull()?.let { replayed ->
|
||||||
if (replayed.none { it.image?.isNotEmpty() == true}) {
|
if (replayed.none { it.image?.isNotEmpty() == true}) {
|
||||||
defaultRooms.tryEmit(earlyGroups)
|
defaultRooms.tryEmit(earlyGroups)
|
||||||
@ -452,12 +419,11 @@ object OpenGroupAPIV2 {
|
|||||||
val images = groups.map { group ->
|
val images = groups.map { group ->
|
||||||
group.id to downloadOpenGroupProfilePicture(group.id, DEFAULT_SERVER)
|
group.id to downloadOpenGroupProfilePicture(group.id, DEFAULT_SERVER)
|
||||||
}.toMap()
|
}.toMap()
|
||||||
|
|
||||||
groups.map { group ->
|
groups.map { group ->
|
||||||
val image = try {
|
val image = try {
|
||||||
images[group.id]!!.get()
|
images[group.id]!!.get()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// no image or image failed to download
|
// No image or image failed to download
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
DefaultGroup(group.id, group.name, image)
|
DefaultGroup(group.id, group.name, image)
|
||||||
@ -470,9 +436,9 @@ object OpenGroupAPIV2 {
|
|||||||
fun getInfo(room: String, server: String): Promise<Info, Exception> {
|
fun getInfo(room: String, server: String): Promise<Info, Exception> {
|
||||||
val request = Request(verb = GET, room = null, server = server, endpoint = "rooms/$room", isAuthRequired = false)
|
val request = Request(verb = GET, room = null, server = server, endpoint = "rooms/$room", isAuthRequired = false)
|
||||||
return send(request).map { json ->
|
return send(request).map { json ->
|
||||||
val rawRoom = json["room"] as? Map<*, *> ?: throw Error.PARSING_FAILED
|
val rawRoom = json["room"] as? Map<*, *> ?: throw Error.ParsingFailed
|
||||||
val id = rawRoom["id"] as? String ?: throw Error.PARSING_FAILED
|
val id = rawRoom["id"] as? String ?: throw Error.ParsingFailed
|
||||||
val name = rawRoom["name"] as? String ?: throw Error.PARSING_FAILED
|
val name = rawRoom["name"] as? String ?: throw Error.ParsingFailed
|
||||||
val imageID = rawRoom["image_id"] as? String
|
val imageID = rawRoom["image_id"] as? String
|
||||||
Info(id = id, name = name, imageID = imageID)
|
Info(id = id, name = name, imageID = imageID)
|
||||||
}
|
}
|
||||||
@ -481,13 +447,13 @@ object OpenGroupAPIV2 {
|
|||||||
fun getAllRooms(server: String): Promise<List<Info>, Exception> {
|
fun getAllRooms(server: String): Promise<List<Info>, Exception> {
|
||||||
val request = Request(verb = GET, room = null, server = server, endpoint = "rooms", isAuthRequired = false)
|
val request = Request(verb = GET, room = null, server = server, endpoint = "rooms", isAuthRequired = false)
|
||||||
return send(request).map { json ->
|
return send(request).map { json ->
|
||||||
val rawRooms = json["rooms"] as? List<Map<*, *>> ?: throw Error.PARSING_FAILED
|
val rawRooms = json["rooms"] as? List<Map<*, *>> ?: throw Error.ParsingFailed
|
||||||
rawRooms.mapNotNull {
|
rawRooms.mapNotNull {
|
||||||
val roomJson = it as? Map<*, *> ?: return@mapNotNull null
|
val roomJson = it as? Map<*, *> ?: return@mapNotNull null
|
||||||
val id = roomJson["id"] as? String ?: return@mapNotNull null
|
val id = roomJson["id"] as? String ?: return@mapNotNull null
|
||||||
val name = roomJson["name"] as? String ?: return@mapNotNull null
|
val name = roomJson["name"] as? String ?: return@mapNotNull null
|
||||||
val imageId = roomJson["image_id"] as? String
|
val imageID = roomJson["image_id"] as? String
|
||||||
Info(id, name, imageId)
|
Info(id, name, imageID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -495,12 +461,11 @@ object OpenGroupAPIV2 {
|
|||||||
fun getMemberCount(room: String, server: String): Promise<Int, Exception> {
|
fun getMemberCount(room: String, server: String): Promise<Int, Exception> {
|
||||||
val request = Request(verb = GET, room = room, server = server, endpoint = "member_count")
|
val request = Request(verb = GET, room = room, server = server, endpoint = "member_count")
|
||||||
return send(request).map { json ->
|
return send(request).map { json ->
|
||||||
val memberCount = json["member_count"] as? Int ?: throw Error.PARSING_FAILED
|
val memberCount = json["member_count"] as? Int ?: throw Error.ParsingFailed
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
storage.setUserCount(room, server, memberCount)
|
storage.setUserCount(room, server, memberCount)
|
||||||
memberCount
|
memberCount
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
}
|
}
|
@ -9,14 +9,18 @@ import org.session.libsignal.utilities.logging.Log
|
|||||||
import org.whispersystems.curve25519.Curve25519
|
import org.whispersystems.curve25519.Curve25519
|
||||||
|
|
||||||
data class OpenGroupMessageV2(
|
data class OpenGroupMessageV2(
|
||||||
val serverID: Long? = null,
|
val serverID: Long? = null,
|
||||||
val sender: String?,
|
val sender: String?,
|
||||||
val sentTimestamp: Long,
|
val sentTimestamp: Long,
|
||||||
// The serialized protobuf in base64 encoding
|
/**
|
||||||
val base64EncodedData: String,
|
* The serialized protobuf in base64 encoding.
|
||||||
// When sending a message, the sender signs the serialized protobuf with their private key so that
|
*/
|
||||||
// a receiving user can verify that the message wasn't tampered with.
|
val base64EncodedData: String,
|
||||||
val base64EncodedSignature: String? = null
|
/**
|
||||||
|
* When sending a message, the sender signs the serialized protobuf with their private key so that
|
||||||
|
* a receiving user can verify that the message wasn't tampered with.
|
||||||
|
*/
|
||||||
|
val base64EncodedSignature: String? = null
|
||||||
) {
|
) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@ -28,11 +32,12 @@ data class OpenGroupMessageV2(
|
|||||||
val serverID = json["server_id"] as? Int
|
val serverID = json["server_id"] as? Int
|
||||||
val sender = json["public_key"] as? String
|
val sender = json["public_key"] as? String
|
||||||
val base64EncodedSignature = json["signature"] as? String
|
val base64EncodedSignature = json["signature"] as? String
|
||||||
return OpenGroupMessageV2(serverID = serverID?.toLong(),
|
return OpenGroupMessageV2(
|
||||||
sender = sender,
|
serverID = serverID?.toLong(),
|
||||||
sentTimestamp = sentTimestamp,
|
sender = sender,
|
||||||
base64EncodedData = base64EncodedData,
|
sentTimestamp = sentTimestamp,
|
||||||
base64EncodedSignature = base64EncodedSignature
|
base64EncodedData = base64EncodedData,
|
||||||
|
base64EncodedSignature = base64EncodedSignature
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,29 +46,26 @@ data class OpenGroupMessageV2(
|
|||||||
fun sign(): OpenGroupMessageV2? {
|
fun sign(): OpenGroupMessageV2? {
|
||||||
if (base64EncodedData.isEmpty()) return null
|
if (base64EncodedData.isEmpty()) return null
|
||||||
val (publicKey, privateKey) = MessagingModuleConfiguration.shared.storage.getUserKeyPair() ?: return null
|
val (publicKey, privateKey) = MessagingModuleConfiguration.shared.storage.getUserKeyPair() ?: return null
|
||||||
|
if (sender != publicKey) return null
|
||||||
if (sender != publicKey) return null // only sign our own messages?
|
|
||||||
|
|
||||||
val signature = try {
|
val signature = try {
|
||||||
curve.calculateSignature(privateKey, decode(base64EncodedData))
|
curve.calculateSignature(privateKey, decode(base64EncodedData))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("Loki", "Couldn't sign OpenGroupV2Message", e)
|
Log.w("Loki", "Couldn't sign open group message.", e)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return copy(base64EncodedSignature = Base64.encodeBytes(signature))
|
return copy(base64EncodedSignature = Base64.encodeBytes(signature))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toJSON(): Map<String, Any> {
|
fun toJSON(): Map<String, Any> {
|
||||||
val jsonMap = mutableMapOf("data" to base64EncodedData, "timestamp" to sentTimestamp)
|
val json = mutableMapOf( "data" to base64EncodedData, "timestamp" to sentTimestamp )
|
||||||
serverID?.let { jsonMap["server_id"] = serverID }
|
serverID?.let { json["server_id"] = it }
|
||||||
sender?.let { jsonMap["public_key"] = sender }
|
sender?.let { json["public_key"] = it }
|
||||||
base64EncodedSignature?.let { jsonMap["signature"] = base64EncodedSignature }
|
base64EncodedSignature?.let { json["signature"] = it }
|
||||||
return jsonMap
|
return json
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toProto(): SignalServiceProtos.Content = decode(base64EncodedData).let(PushTransportDetails::getStrippedPaddingMessageBody).let { bytes ->
|
fun toProto(): SignalServiceProtos.Content {
|
||||||
SignalServiceProtos.Content.parseFrom(bytes)
|
val data = decode(base64EncodedData).let(PushTransportDetails::getStrippedPaddingMessageBody)
|
||||||
|
return SignalServiceProtos.Content.parseFrom(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -1,51 +1,50 @@
|
|||||||
package org.session.libsession.messaging.open_groups
|
package org.session.libsession.messaging.open_groups
|
||||||
|
|
||||||
import org.session.libsignal.utilities.JsonUtil
|
import org.session.libsignal.utilities.JsonUtil
|
||||||
|
import org.session.libsignal.utilities.logging.Log
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
data class OpenGroupV2(
|
data class OpenGroupV2(
|
||||||
val server: String,
|
val server: String,
|
||||||
val room: String,
|
val room: String,
|
||||||
val id: String,
|
val id: String,
|
||||||
val name: String,
|
val name: String,
|
||||||
val publicKey: String
|
val publicKey: String
|
||||||
) {
|
) {
|
||||||
|
|
||||||
constructor(server: String, room: String, name: String, publicKey: String) : this(
|
constructor(server: String, room: String, name: String, publicKey: String) : this(
|
||||||
server = server,
|
server = server,
|
||||||
room = room,
|
room = room,
|
||||||
id = "$server.$room",
|
id = "$server.$room",
|
||||||
name = name,
|
name = name,
|
||||||
publicKey = publicKey,
|
publicKey = publicKey,
|
||||||
)
|
)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
fun fromJson(jsonAsString: String): OpenGroupV2? {
|
fun fromJSON(jsonAsString: String): OpenGroupV2? {
|
||||||
return try {
|
return try {
|
||||||
val json = JsonUtil.fromJson(jsonAsString)
|
val json = JsonUtil.fromJson(jsonAsString)
|
||||||
if (!json.has("room")) return null
|
if (!json.has("room")) return null
|
||||||
|
val room = json.get("room").asText().toLowerCase(Locale.US)
|
||||||
val room = json.get("room").asText().toLowerCase(Locale.getDefault())
|
val server = json.get("server").asText().toLowerCase(Locale.US)
|
||||||
val server = json.get("server").asText().toLowerCase(Locale.getDefault())
|
|
||||||
val displayName = json.get("displayName").asText()
|
val displayName = json.get("displayName").asText()
|
||||||
val publicKey = json.get("publicKey").asText()
|
val publicKey = json.get("publicKey").asText()
|
||||||
|
|
||||||
OpenGroupV2(server, room, displayName, publicKey)
|
OpenGroupV2(server, room, displayName, publicKey)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
Log.w("Loki", "Couldn't parse open group from JSON: $jsonAsString.", e);
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toJoinUrl(): String = "$server/$room?public_key=$publicKey"
|
|
||||||
|
|
||||||
fun toJson(): Map<String,String> = mapOf(
|
fun toJson(): Map<String,String> = mapOf(
|
||||||
"room" to room,
|
"room" to room,
|
||||||
"server" to server,
|
"server" to server,
|
||||||
"displayName" to name,
|
"displayName" to name,
|
||||||
"publicKey" to publicKey,
|
"publicKey" to publicKey,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val joinURL: String get() = "$server/$room?public_key=$publicKey"
|
||||||
}
|
}
|
@ -258,11 +258,11 @@ object MessageSender {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val proto = message.toProto()!!
|
val proto = message.toProto()!!
|
||||||
|
val plaintext = PushTransportDetails.getPaddedMessageBody(proto.toByteArray())
|
||||||
val openGroupMessage = OpenGroupMessageV2(
|
val openGroupMessage = OpenGroupMessageV2(
|
||||||
sender = message.sender,
|
sender = message.sender,
|
||||||
sentTimestamp = message.sentTimestamp!!,
|
sentTimestamp = message.sentTimestamp!!,
|
||||||
base64EncodedData = Base64.encodeBytes(proto.toByteArray()),
|
base64EncodedData = Base64.encodeBytes(plaintext),
|
||||||
)
|
)
|
||||||
|
|
||||||
OpenGroupAPIV2.send(openGroupMessage,room,server).success {
|
OpenGroupAPIV2.send(openGroupMessage,room,server).success {
|
||||||
|
@ -125,7 +125,7 @@ private fun handleConfigurationMessage(message: ConfigurationMessage) {
|
|||||||
handleNewClosedGroup(message.sender!!, message.sentTimestamp!!, closeGroup.publicKey, closeGroup.name, closeGroup.encryptionKeyPair!!, closeGroup.members, closeGroup.admins, message.sentTimestamp!!)
|
handleNewClosedGroup(message.sender!!, message.sentTimestamp!!, closeGroup.publicKey, closeGroup.name, closeGroup.encryptionKeyPair!!, closeGroup.members, closeGroup.admins, message.sentTimestamp!!)
|
||||||
}
|
}
|
||||||
val allOpenGroups = storage.getAllOpenGroups().map { it.value.server }
|
val allOpenGroups = storage.getAllOpenGroups().map { it.value.server }
|
||||||
val allV2OpenGroups = storage.getAllV2OpenGroups().map { it.value.toJoinUrl() }
|
val allV2OpenGroups = storage.getAllV2OpenGroups().map { it.value.joinURL }
|
||||||
for (openGroup in message.openGroups) {
|
for (openGroup in message.openGroups) {
|
||||||
if (allOpenGroups.contains(openGroup) || allV2OpenGroups.contains(openGroup)) continue
|
if (allOpenGroups.contains(openGroup) || allV2OpenGroups.contains(openGroup)) continue
|
||||||
storage.addOpenGroup(openGroup, 1)
|
storage.addOpenGroup(openGroup, 1)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user