mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-27 12:05:22 +00:00
Merge branch 'dev' of https://github.com/oxen-io/session-android into client-side-nickname
This commit is contained in:
commit
1e3ff1e6a1
@ -158,8 +158,8 @@ dependencies {
|
||||
testImplementation 'org.robolectric:shadows-multidex:4.2'
|
||||
}
|
||||
|
||||
def canonicalVersionCode = 158
|
||||
def canonicalVersionName = "1.10.1"
|
||||
def canonicalVersionCode = 159
|
||||
def canonicalVersionName = "1.10.2"
|
||||
|
||||
def postFixSize = 10
|
||||
def abiPostFix = ['armeabi-v7a' : 1,
|
||||
|
@ -252,7 +252,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
||||
val database = databaseHelper.readableDatabase
|
||||
return database.get(LokiThreadDatabase.publicChatTable, "${LokiThreadDatabase.threadID} = ?", arrayOf(threadId)) { cursor ->
|
||||
val publicChatAsJson = cursor.getString(LokiThreadDatabase.publicChat)
|
||||
OpenGroupV2.fromJson(publicChatAsJson)
|
||||
OpenGroupV2.fromJSON(publicChatAsJson)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -195,7 +195,7 @@ class EnterChatURLFragment : Fragment() {
|
||||
chip.chipIcon = drawable
|
||||
chip.text = defaultGroup.name
|
||||
chip.setOnClickListener {
|
||||
(requireActivity() as JoinPublicChatActivity).joinPublicChatIfPossible(defaultGroup.toJoinUrl())
|
||||
(requireActivity() as JoinPublicChatActivity).joinPublicChatIfPossible(defaultGroup.joinURL)
|
||||
}
|
||||
defaultRoomsGridLayout.addView(chip)
|
||||
}
|
||||
|
@ -68,7 +68,7 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
val threadID = cursor.getLong(threadID)
|
||||
val string = cursor.getString(publicChat)
|
||||
val openGroup = OpenGroupV2.fromJson(string)
|
||||
val openGroup = OpenGroupV2.fromJSON(string)
|
||||
if (openGroup != null) result[threadID] = openGroup
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
@ -100,7 +100,7 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
|
||||
val database = databaseHelper.readableDatabase
|
||||
return database.get(publicChatTable, "${Companion.threadID} = ?", arrayOf(threadID.toString())) { cursor ->
|
||||
val json = cursor.getString(publicChat)
|
||||
OpenGroupV2.fromJson(json)
|
||||
OpenGroupV2.fromJSON(json)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,106 +1,91 @@
|
||||
package org.session.libsession.messaging.file_server
|
||||
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.functional.bind
|
||||
import nl.komponents.kovenant.functional.map
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.RequestBody
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
|
||||
import org.session.libsession.snode.OnionRequestAPI
|
||||
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.JsonUtil
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
|
||||
object FileServerAPIV2 {
|
||||
|
||||
const val DEFAULT_SERVER = "http://88.99.175.227"
|
||||
private const val DEFAULT_SERVER_PUBLIC_KEY = "7cb31905b55cd5580c686911debf672577b3fb0bff81df4ce2d5c4cb3a7aaa69"
|
||||
const val DEFAULT_SERVER = "http://88.99.175.227"
|
||||
|
||||
sealed class Error : Exception() {
|
||||
object PARSING_FAILED : Error()
|
||||
object INVALID_URL : Error()
|
||||
|
||||
fun errorDescription() = when (this) {
|
||||
PARSING_FAILED -> "Invalid response."
|
||||
INVALID_URL -> "Invalid URL."
|
||||
}
|
||||
|
||||
sealed class Error(message: String) : Exception(message) {
|
||||
object ParsingFailed : Error("Invalid response.")
|
||||
object InvalidURL : Error("Invalid URL.")
|
||||
}
|
||||
|
||||
data class Request(
|
||||
val verb: HTTP.Verb,
|
||||
val endpoint: String,
|
||||
val queryParameters: Map<String, String> = mapOf(),
|
||||
val parameters: Any? = null,
|
||||
val headers: Map<String, String> = mapOf(),
|
||||
// Always `true` under normal circumstances. You might want to disable
|
||||
// this when running over Lokinet.
|
||||
val useOnionRouting: Boolean = true
|
||||
val verb: HTTP.Verb,
|
||||
val endpoint: String,
|
||||
val queryParameters: Map<String, String> = mapOf(),
|
||||
val parameters: Any? = null,
|
||||
val headers: Map<String, String> = mapOf(),
|
||||
/**
|
||||
* Always `true` under normal circumstances. You might want to disable
|
||||
* this when running over Lokinet.
|
||||
*/
|
||||
val useOnionRouting: Boolean = true
|
||||
)
|
||||
|
||||
private fun createBody(parameters: Any?): RequestBody? {
|
||||
if (parameters == null) return null
|
||||
|
||||
val parametersAsJSON = JsonUtil.toJson(parameters)
|
||||
return RequestBody.create(MediaType.get("application/json"), parametersAsJSON)
|
||||
}
|
||||
|
||||
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()
|
||||
.scheme(parsed.scheme())
|
||||
.host(parsed.host())
|
||||
.port(parsed.port())
|
||||
.addPathSegments(request.endpoint)
|
||||
|
||||
.scheme(url.scheme())
|
||||
.host(url.host())
|
||||
.port(url.port())
|
||||
.addPathSegments(request.endpoint)
|
||||
if (request.verb == HTTP.Verb.GET) {
|
||||
for ((key, value) in request.queryParameters) {
|
||||
urlBuilder.addQueryParameter(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
val requestBuilder = okhttp3.Request.Builder()
|
||||
.url(urlBuilder.build())
|
||||
.headers(Headers.of(request.headers))
|
||||
.url(urlBuilder.build())
|
||||
.headers(Headers.of(request.headers))
|
||||
when (request.verb) {
|
||||
HTTP.Verb.GET -> requestBuilder.get()
|
||||
HTTP.Verb.PUT -> requestBuilder.put(createBody(request.parameters)!!)
|
||||
HTTP.Verb.POST -> requestBuilder.post(createBody(request.parameters)!!)
|
||||
HTTP.Verb.DELETE -> requestBuilder.delete(createBody(request.parameters))
|
||||
}
|
||||
|
||||
if (request.useOnionRouting) {
|
||||
return OnionRequestAPI.sendOnionRequest(requestBuilder.build(), DEFAULT_SERVER, DEFAULT_SERVER_PUBLIC_KEY)
|
||||
.fail { e ->
|
||||
Log.e("Loki", "FileServerV2 failed with error",e)
|
||||
}
|
||||
return OnionRequestAPI.sendOnionRequest(requestBuilder.build(), DEFAULT_SERVER, DEFAULT_SERVER_PUBLIC_KEY).fail { e ->
|
||||
Log.e("Loki", "File server request failed.", e)
|
||||
}
|
||||
} else {
|
||||
return Promise.ofFail(IllegalStateException("It's currently not allowed to send non onion routed requests."))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// region Sending
|
||||
fun upload(file: ByteArray): Promise<Long, Exception> {
|
||||
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)
|
||||
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> {
|
||||
val request = Request(verb = HTTP.Verb.GET, endpoint = "files/$file")
|
||||
return send(request).map { json ->
|
||||
val base64EncodedFile = json["result"] as? String ?: throw Error.PARSING_FAILED
|
||||
Base64.decode(base64EncodedFile) ?: throw Error.PARSING_FAILED
|
||||
val base64EncodedFile = json["result"] as? String ?: throw Error.ParsingFailed
|
||||
Base64.decode(base64EncodedFile) ?: throw Error.ParsingFailed
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -7,9 +7,6 @@ import org.session.libsession.messaging.threads.Address
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsignal.service.loki.utilities.toHexString
|
||||
|
||||
typealias OpenGroupModel = OpenGroup
|
||||
typealias OpenGroupV2Model = OpenGroupV2
|
||||
|
||||
sealed class Destination {
|
||||
|
||||
class Contact(var publicKey: String) : Destination() {
|
||||
@ -21,11 +18,12 @@ sealed class Destination {
|
||||
class OpenGroup(var channel: Long, var server: String) : Destination() {
|
||||
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("", "")
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun from(address: Address): Destination {
|
||||
return when {
|
||||
address.isContact -> {
|
||||
@ -39,10 +37,12 @@ sealed class Destination {
|
||||
address.isOpenGroup -> {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val threadID = storage.getThreadID(address.contactIdentifier())!!
|
||||
when (val openGroup = storage.getOpenGroup(threadID) ?: storage.getV2OpenGroup(threadID)) {
|
||||
is OpenGroupModel -> OpenGroup(openGroup.channel, openGroup.server)
|
||||
is OpenGroupV2Model -> OpenGroupV2(openGroup.room, openGroup.server)
|
||||
else -> throw Exception("Invalid OpenGroup $openGroup")
|
||||
when (val openGroup = storage.getV2OpenGroup(threadID) ?: storage.getOpenGroup(threadID)) {
|
||||
is org.session.libsession.messaging.open_groups.OpenGroup
|
||||
-> Destination.OpenGroup(openGroup.channel, openGroup.server)
|
||||
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 -> {
|
||||
|
@ -18,12 +18,10 @@ abstract class Message {
|
||||
open val isSelfSendValid: Boolean = false
|
||||
|
||||
open fun isValid(): Boolean {
|
||||
sentTimestamp?.let {
|
||||
if (it <= 0) return false
|
||||
}
|
||||
receivedTimestamp?.let {
|
||||
if (it <= 0) return false
|
||||
}
|
||||
val sentTimestamp = sentTimestamp
|
||||
if (sentTimestamp != null && sentTimestamp <= 0) { return false }
|
||||
val receivedTimestamp = receivedTimestamp
|
||||
if (receivedTimestamp != null && receivedTimestamp <= 0) { return false }
|
||||
return sender != null && recipient != null
|
||||
}
|
||||
|
||||
|
@ -16,9 +16,10 @@ import org.session.libsignal.utilities.Hex
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
|
||||
class ClosedGroupControlMessage() : ControlMessage() {
|
||||
var kind: Kind? = null
|
||||
|
||||
override val ttl: Long = run {
|
||||
when (kind) {
|
||||
override val ttl: Long get() {
|
||||
return when (kind) {
|
||||
is Kind.EncryptionKeyPair -> 14 * 24 * 60 * 60 * 1000
|
||||
else -> 14 * 24 * 60 * 60 * 1000
|
||||
}
|
||||
@ -26,31 +27,46 @@ class ClosedGroupControlMessage() : ControlMessage() {
|
||||
|
||||
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 {
|
||||
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.
|
||||
///
|
||||
/// - Note: `publicKey` is only set when an encryption key pair is sent in a one-to-one context (i.e. not in a group).
|
||||
/** 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).
|
||||
*/
|
||||
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() {
|
||||
internal constructor(): this("")
|
||||
internal constructor() : this("")
|
||||
}
|
||||
class MembersAdded(var members: List<ByteString>) : Kind() {
|
||||
internal constructor(): this(listOf())
|
||||
internal constructor() : this(listOf())
|
||||
}
|
||||
class MembersRemoved(var members: List<ByteString>) : Kind() {
|
||||
internal constructor(): this(listOf())
|
||||
internal constructor() : this(listOf())
|
||||
}
|
||||
class MemberLeft() : Kind()
|
||||
|
||||
val description: String =
|
||||
when(this) {
|
||||
when (this) {
|
||||
is New -> "new"
|
||||
is EncryptionKeyPair -> "encryptionKeyPair"
|
||||
is NameChange -> "nameChange"
|
||||
@ -65,18 +81,19 @@ class ClosedGroupControlMessage() : ControlMessage() {
|
||||
|
||||
fun fromProto(proto: SignalServiceProtos.Content): ClosedGroupControlMessage? {
|
||||
if (!proto.hasDataMessage() || !proto.dataMessage.hasClosedGroupControlMessage()) return null
|
||||
val closedGroupControlMessageProto = proto.dataMessage?.closedGroupControlMessage!!
|
||||
val closedGroupControlMessageProto = proto.dataMessage!!.closedGroupControlMessage!!
|
||||
val kind: Kind
|
||||
when (closedGroupControlMessageProto.type) {
|
||||
when (closedGroupControlMessageProto.type!!) {
|
||||
DataMessage.ClosedGroupControlMessage.Type.NEW -> {
|
||||
val publicKey = closedGroupControlMessageProto.publicKey ?: return null
|
||||
val name = closedGroupControlMessageProto.name ?: return null
|
||||
val encryptionKeyPairAsProto = closedGroupControlMessageProto.encryptionKeyPair ?: return null
|
||||
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)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Couldn't parse key pair")
|
||||
Log.w(TAG, "Couldn't parse key pair from proto: $encryptionKeyPairAsProto.")
|
||||
return null
|
||||
}
|
||||
}
|
||||
@ -107,26 +124,10 @@ class ClosedGroupControlMessage() : ControlMessage() {
|
||||
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? {
|
||||
val kind = kind
|
||||
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
|
||||
}
|
||||
try {
|
||||
@ -176,7 +177,7 @@ class ClosedGroupControlMessage() : ControlMessage() {
|
||||
contentProto.dataMessage = dataMessageProto.build()
|
||||
return contentProto.build()
|
||||
} 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
|
||||
}
|
||||
}
|
||||
@ -188,6 +189,7 @@ class ClosedGroupControlMessage() : ControlMessage() {
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun fromProto(proto: DataMessage.ClosedGroupControlMessage.KeyPairWrapper): KeyPairWrapper {
|
||||
return KeyPairWrapper(proto.publicKey.toByteArray().toHexString(), proto.encryptedKeyPair)
|
||||
}
|
||||
@ -199,7 +201,6 @@ class ClosedGroupControlMessage() : ControlMessage() {
|
||||
val result = DataMessage.ClosedGroupControlMessage.KeyPairWrapper.newBuilder()
|
||||
result.publicKey = ByteString.copyFrom(Hex.fromStringCondensed(publicKey))
|
||||
result.encryptedKeyPair = encryptedKeyPair
|
||||
|
||||
return try {
|
||||
result.build()
|
||||
} 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.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>) {
|
||||
val isValid: Boolean get() = members.isNotEmpty() && admins.isNotEmpty()
|
||||
|
||||
internal constructor(): this("", "", null, listOf(), listOf())
|
||||
internal constructor() : this("", "", null, listOf(), listOf())
|
||||
|
||||
override fun toString(): String {
|
||||
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?) {
|
||||
|
||||
internal constructor(): this("", "", null, null)
|
||||
internal constructor() : this("", "", null, null)
|
||||
|
||||
companion object {
|
||||
|
||||
@ -66,8 +69,7 @@ class ConfigurationMessage(var closedGroups: List<ClosedGroup>, var openGroups:
|
||||
val name = proto.name
|
||||
val profilePicture = if (proto.hasProfilePicture()) proto.profilePicture 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) {
|
||||
return null
|
||||
}
|
||||
if (!this.profilePicture.isNullOrEmpty()) {
|
||||
result.profilePicture = this.profilePicture
|
||||
val profilePicture = profilePicture
|
||||
if (!profilePicture.isNullOrEmpty()) {
|
||||
result.profilePicture = profilePicture
|
||||
}
|
||||
if (this.profileKey != null) {
|
||||
result.profileKey = ByteString.copyFrom(this.profileKey)
|
||||
val profileKey = profileKey
|
||||
if (profileKey != null) {
|
||||
result.profileKey = ByteString.copyFrom(profileKey)
|
||||
}
|
||||
return result.build()
|
||||
}
|
||||
}
|
||||
|
||||
override val isSelfSendValid: Boolean = true
|
||||
|
||||
companion object {
|
||||
|
||||
fun getCurrent(contacts: List<Contact>): ConfigurationMessage? {
|
||||
@ -103,24 +105,22 @@ class ConfigurationMessage(var closedGroups: List<ClosedGroup>, var openGroups:
|
||||
val profilePicture = TextSecurePreferences.getProfilePictureURL(context)
|
||||
val profileKey = ProfileKeyUtil.getProfileKey(context)
|
||||
val groups = storage.getAllGroups()
|
||||
for (groupRecord in groups) {
|
||||
if (groupRecord.isClosedGroup) {
|
||||
if (!groupRecord.members.contains(Address.fromSerialized(storage.getUserPublicKey()!!))) continue
|
||||
val groupPublicKey = GroupUtil.doubleDecodeGroupID(groupRecord.encodedId).toHexString()
|
||||
for (group in groups) {
|
||||
if (group.isClosedGroup) {
|
||||
if (!group.members.contains(Address.fromSerialized(storage.getUserPublicKey()!!))) continue
|
||||
val groupPublicKey = GroupUtil.doubleDecodeGroupID(group.encodedId).toHexString()
|
||||
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)
|
||||
}
|
||||
if (groupRecord.isOpenGroup) {
|
||||
val threadID = storage.getThreadID(groupRecord.encodedId) ?: continue
|
||||
if (group.isOpenGroup) {
|
||||
val threadID = storage.getThreadID(group.encodedId) ?: continue
|
||||
val openGroup = storage.getOpenGroup(threadID)
|
||||
val openGroupV2 = storage.getV2OpenGroup(threadID)
|
||||
|
||||
val shareUrl = openGroup?.server ?: openGroupV2?.toJoinUrl() ?: continue
|
||||
val shareUrl = openGroup?.server ?: openGroupV2?.joinURL ?: continue
|
||||
openGroups.add(shareUrl)
|
||||
}
|
||||
}
|
||||
|
||||
return ConfigurationMessage(closedGroups, openGroups, contacts, displayName, profilePicture, profileKey)
|
||||
}
|
||||
|
||||
@ -145,6 +145,7 @@ class ConfigurationMessage(var closedGroups: List<ClosedGroup>, var openGroups:
|
||||
configurationProto.addAllOpenGroups(openGroups)
|
||||
configurationProto.addAllContacts(this.contacts.mapNotNull { it.toProto() })
|
||||
configurationProto.displayName = displayName
|
||||
val profilePicture = profilePicture
|
||||
if (!profilePicture.isNullOrEmpty()) {
|
||||
configurationProto.profilePicture = profilePicture
|
||||
}
|
||||
@ -157,10 +158,10 @@ class ConfigurationMessage(var closedGroups: List<ClosedGroup>, var openGroups:
|
||||
override fun toString(): String {
|
||||
return """
|
||||
ConfigurationMessage(
|
||||
closedGroups: ${(closedGroups)}
|
||||
openGroups: ${(openGroups)}
|
||||
displayName: $displayName
|
||||
profilePicture: $profilePicture
|
||||
closedGroups: ${(closedGroups)},
|
||||
openGroups: ${(openGroups)},
|
||||
displayName: $displayName,
|
||||
profilePicture: $profilePicture,
|
||||
profileKey: $profileKey
|
||||
)
|
||||
""".trimIndent()
|
||||
|
@ -2,5 +2,4 @@ package org.session.libsession.messaging.messages.control
|
||||
|
||||
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.utilities.logging.Log
|
||||
|
||||
class DataExtractionNotification(): ControlMessage() {
|
||||
class DataExtractionNotification() : ControlMessage() {
|
||||
var kind: Kind? = null
|
||||
|
||||
sealed class Kind {
|
||||
@ -39,8 +39,8 @@ class DataExtractionNotification(): ControlMessage() {
|
||||
}
|
||||
|
||||
override fun isValid(): Boolean {
|
||||
if (!super.isValid()) return false
|
||||
val kind = kind ?: return false
|
||||
val kind = kind
|
||||
if (!super.isValid() || kind == null) return false
|
||||
return when(kind) {
|
||||
is Kind.Screenshot -> true
|
||||
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
|
||||
|
||||
class ExpirationTimerUpdate() : ControlMessage() {
|
||||
/// In the case of a sync message, the public key of the person the message was targeted at.
|
||||
/// - Note: `nil` if this isn't a sync message.
|
||||
/** In the case of a sync message, the public key of the person the message was targeted at.
|
||||
*
|
||||
* **Note:** `nil` if this isn't a sync message.
|
||||
*/
|
||||
var syncTarget: String? = null
|
||||
var duration: Int? = 0
|
||||
|
||||
override val isSelfSendValid: Boolean = true
|
||||
|
||||
override fun isValid(): Boolean {
|
||||
if (!super.isValid()) return false
|
||||
return duration != null
|
||||
}
|
||||
|
||||
companion object {
|
||||
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() {
|
||||
this.syncTarget = null
|
||||
this.duration = duration
|
||||
}
|
||||
|
||||
override fun isValid(): Boolean {
|
||||
if (!super.isValid()) return false
|
||||
return duration != null
|
||||
internal constructor(syncTarget: String, duration: Int) : this() {
|
||||
this.syncTarget = syncTarget
|
||||
this.duration = duration
|
||||
}
|
||||
|
||||
override fun toProto(): SignalServiceProtos.Content? {
|
||||
|
@ -6,6 +6,13 @@ import org.session.libsignal.utilities.logging.Log
|
||||
class ReadReceipt() : ControlMessage() {
|
||||
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 {
|
||||
const val TAG = "ReadReceipt"
|
||||
|
||||
@ -22,13 +29,6 @@ class ReadReceipt() : ControlMessage() {
|
||||
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? {
|
||||
val timestamps = timestamps
|
||||
if (timestamps == null) {
|
||||
|
@ -4,9 +4,15 @@ import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
|
||||
class TypingIndicator() : ControlMessage() {
|
||||
override val ttl: Long = 30 * 1000
|
||||
var kind: Kind? = null
|
||||
|
||||
override val ttl: Long = 20 * 1000
|
||||
|
||||
override fun isValid(): Boolean {
|
||||
if (!super.isValid()) return false
|
||||
return kind != null
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "TypingIndicator"
|
||||
|
||||
@ -41,11 +47,6 @@ class TypingIndicator() : ControlMessage() {
|
||||
this.kind = kind
|
||||
}
|
||||
|
||||
override fun isValid(): Boolean {
|
||||
if (!super.isValid()) return false
|
||||
return kind != null
|
||||
}
|
||||
|
||||
override fun toProto(): SignalServiceProtos.Content? {
|
||||
val timestamp = sentTimestamp
|
||||
val kind = kind
|
||||
|
@ -10,6 +10,10 @@ class LinkPreview() {
|
||||
var url: String? = null
|
||||
var attachmentID: Long? = 0
|
||||
|
||||
fun isValid(): Boolean {
|
||||
return (title != null && url != null && attachmentID != null)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "LinkPreview"
|
||||
|
||||
@ -20,11 +24,8 @@ class LinkPreview() {
|
||||
}
|
||||
|
||||
fun from(signalLinkPreview: SignalLinkPreiview?): LinkPreview? {
|
||||
return if (signalLinkPreview == null) {
|
||||
null
|
||||
} else {
|
||||
LinkPreview(signalLinkPreview.title, signalLinkPreview.url, signalLinkPreview.attachmentId?.rowId)
|
||||
}
|
||||
if (signalLinkPreview == null) { return null }
|
||||
return LinkPreview(signalLinkPreview.title, signalLinkPreview.url, signalLinkPreview.attachmentId?.rowId)
|
||||
}
|
||||
}
|
||||
|
||||
@ -34,10 +35,6 @@ class LinkPreview() {
|
||||
this.attachmentID = attachmentID
|
||||
}
|
||||
|
||||
fun isValid(): Boolean {
|
||||
return (title != null && url != null && attachmentID != null)
|
||||
}
|
||||
|
||||
fun toProto(): SignalServiceProtos.DataMessage.Preview? {
|
||||
val url = url
|
||||
if (url == null) {
|
||||
@ -46,10 +43,10 @@ class LinkPreview() {
|
||||
}
|
||||
val linkPreviewProto = SignalServiceProtos.DataMessage.Preview.newBuilder()
|
||||
linkPreviewProto.url = url
|
||||
title?.let { linkPreviewProto.title = title }
|
||||
val attachmentID = attachmentID
|
||||
title?.let { linkPreviewProto.title = it }
|
||||
val database = MessagingModuleConfiguration.shared.messageDataProvider
|
||||
attachmentID?.let {
|
||||
MessagingModuleConfiguration.shared.messageDataProvider.getSignalAttachmentPointer(attachmentID)?.let {
|
||||
database.getSignalAttachmentPointer(it)?.let {
|
||||
val attachmentProto = Attachment.createAttachmentPointer(it)
|
||||
linkPreviewProto.image = attachmentProto
|
||||
}
|
||||
|
@ -17,12 +17,11 @@ class Profile() {
|
||||
val displayName = profileProto.displayName ?: return null
|
||||
val profileKey = proto.profileKey
|
||||
val profilePictureURL = profileProto.profilePicture
|
||||
profileKey?.let {
|
||||
profilePictureURL?.let {
|
||||
return Profile(displayName = displayName, profileKey = profileKey.toByteArray(), profilePictureURL = profilePictureURL)
|
||||
}
|
||||
if (profileKey != null && profilePictureURL != null) {
|
||||
return Profile(displayName, profileKey.toByteArray(), profilePictureURL)
|
||||
} else {
|
||||
return Profile(displayName)
|
||||
}
|
||||
return Profile(displayName)
|
||||
}
|
||||
}
|
||||
|
||||
@ -35,16 +34,14 @@ class Profile() {
|
||||
fun toProto(): SignalServiceProtos.DataMessage? {
|
||||
val displayName = displayName
|
||||
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
|
||||
}
|
||||
val dataMessageProto = SignalServiceProtos.DataMessage.newBuilder()
|
||||
val profileProto = SignalServiceProtos.DataMessage.LokiProfile.newBuilder()
|
||||
profileProto.displayName = displayName
|
||||
val profileKey = profileKey
|
||||
profileKey?.let { dataMessageProto.profileKey = ByteString.copyFrom(profileKey) }
|
||||
val profilePictureURL = profilePictureURL
|
||||
profilePictureURL?.let { profileProto.profilePicture = profilePictureURL }
|
||||
profileKey?.let { dataMessageProto.profileKey = ByteString.copyFrom(it) }
|
||||
profilePictureURL?.let { profileProto.profilePicture = it }
|
||||
// Build
|
||||
try {
|
||||
dataMessageProto.profile = profileProto.build()
|
||||
|
@ -13,6 +13,10 @@ class Quote() {
|
||||
var text: String? = null
|
||||
var attachmentID: Long? = null
|
||||
|
||||
fun isValid(): Boolean {
|
||||
return (timestamp != null && publicKey != null)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "Quote"
|
||||
|
||||
@ -24,12 +28,9 @@ class Quote() {
|
||||
}
|
||||
|
||||
fun from(signalQuote: SignalQuote?): Quote? {
|
||||
return if (signalQuote == null) {
|
||||
null
|
||||
} else {
|
||||
val attachmentID = (signalQuote.attachments?.firstOrNull() as? DatabaseAttachment)?.attachmentId?.rowId
|
||||
Quote(signalQuote.id, signalQuote.author.serialize(), signalQuote.text, attachmentID)
|
||||
}
|
||||
if (signalQuote == null) { return null }
|
||||
val attachmentID = (signalQuote.attachments?.firstOrNull() as? DatabaseAttachment)?.attachmentId?.rowId
|
||||
return Quote(signalQuote.id, signalQuote.author.serialize(), signalQuote.text, attachmentID)
|
||||
}
|
||||
}
|
||||
|
||||
@ -40,10 +41,6 @@ class Quote() {
|
||||
this.attachmentID = attachmentID
|
||||
}
|
||||
|
||||
fun isValid(): Boolean {
|
||||
return (timestamp != null && publicKey != null)
|
||||
}
|
||||
|
||||
fun toProto(): SignalServiceProtos.DataMessage.Quote? {
|
||||
val timestamp = timestamp
|
||||
val publicKey = publicKey
|
||||
@ -54,7 +51,7 @@ class Quote() {
|
||||
val quoteProto = SignalServiceProtos.DataMessage.Quote.newBuilder()
|
||||
quoteProto.id = timestamp
|
||||
quoteProto.author = publicKey
|
||||
text?.let { quoteProto.text = text }
|
||||
text?.let { quoteProto.text = it }
|
||||
addAttachmentsIfNeeded(quoteProto)
|
||||
// Build
|
||||
try {
|
||||
@ -66,23 +63,23 @@ class Quote() {
|
||||
}
|
||||
|
||||
private fun addAttachmentsIfNeeded(quoteProto: SignalServiceProtos.DataMessage.Quote.Builder) {
|
||||
if (attachmentID == null) return
|
||||
val attachment = MessagingModuleConfiguration.shared.messageDataProvider.getSignalAttachmentPointer(attachmentID!!)
|
||||
if (attachment == null) {
|
||||
val attachmentID = attachmentID ?: return
|
||||
val database = MessagingModuleConfiguration.shared.messageDataProvider
|
||||
val pointer = database.getSignalAttachmentPointer(attachmentID)
|
||||
if (pointer == null) {
|
||||
Log.w(TAG, "Ignoring invalid attachment for quoted message.")
|
||||
return
|
||||
}
|
||||
if (attachment.url.isNullOrEmpty()) {
|
||||
if (pointer.url.isNullOrEmpty()) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
//TODO equivalent to iOS's preconditionFailure
|
||||
Log.d(TAG,"Sending a message before all associated attachments have been uploaded.")
|
||||
Log.w(TAG,"Sending a message before all associated attachments have been uploaded.")
|
||||
return
|
||||
}
|
||||
}
|
||||
val quotedAttachmentProto = SignalServiceProtos.DataMessage.Quote.QuotedAttachment.newBuilder()
|
||||
quotedAttachmentProto.contentType = attachment.contentType
|
||||
if (attachment.fileName.isPresent) quotedAttachmentProto.fileName = attachment.fileName.get()
|
||||
quotedAttachmentProto.thumbnail = Attachment.createAttachmentPointer(attachment)
|
||||
quotedAttachmentProto.contentType = pointer.contentType
|
||||
if (pointer.fileName.isPresent) { quotedAttachmentProto.fileName = pointer.fileName.get() }
|
||||
quotedAttachmentProto.thumbnail = Attachment.createAttachmentPointer(pointer)
|
||||
try {
|
||||
quoteProto.addAttachments(quotedAttachmentProto.build())
|
||||
} 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
|
||||
|
||||
class VisibleMessage : Message() {
|
||||
/** In the case of a sync message, the public key of the person the message was targeted at.
|
||||
*
|
||||
* **Note:** `nil` if this isn't a sync message.
|
||||
*/
|
||||
var syncTarget: String? = null
|
||||
var text: String? = null
|
||||
val attachmentIDs: MutableList<Long> = mutableListOf()
|
||||
@ -21,46 +25,7 @@ class VisibleMessage : Message() {
|
||||
|
||||
override val isSelfSendValid: Boolean = true
|
||||
|
||||
companion object {
|
||||
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
|
||||
}
|
||||
|
||||
// region Validation
|
||||
override fun isValid(): Boolean {
|
||||
if (!super.isValid()) return false
|
||||
if (attachmentIDs.isNotEmpty()) return true
|
||||
@ -68,56 +33,84 @@ class VisibleMessage : Message() {
|
||||
if (text.isNotEmpty()) return true
|
||||
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? {
|
||||
val proto = SignalServiceProtos.Content.newBuilder()
|
||||
val dataMessage: SignalServiceProtos.DataMessage.Builder
|
||||
// Profile
|
||||
val profile = profile
|
||||
val profileProto = profile?.toProto()
|
||||
val profileProto = profile?.let { it.toProto() }
|
||||
if (profileProto != null) {
|
||||
dataMessage = profileProto.toBuilder()
|
||||
} else {
|
||||
dataMessage = SignalServiceProtos.DataMessage.newBuilder()
|
||||
}
|
||||
// Text
|
||||
text?.let { dataMessage.body = text }
|
||||
if (text != null) { dataMessage.body = text }
|
||||
// Quote
|
||||
quote?.let {
|
||||
val quoteProto = it.toProto()
|
||||
if (quoteProto != null) dataMessage.quote = quoteProto
|
||||
val quoteProto = quote?.let { it.toProto() }
|
||||
if (quoteProto != null) {
|
||||
dataMessage.quote = quoteProto
|
||||
}
|
||||
//Link preview
|
||||
linkPreview?.let {
|
||||
val linkPreviewProto = it.toProto()
|
||||
linkPreviewProto?.let {
|
||||
dataMessage.addAllPreview(listOf(linkPreviewProto))
|
||||
}
|
||||
// Link preview
|
||||
val linkPreviewProto = linkPreview?.let { it.toProto() }
|
||||
if (linkPreviewProto != null) {
|
||||
dataMessage.addAllPreview(listOf(linkPreviewProto))
|
||||
}
|
||||
//Attachments
|
||||
val attachments = attachmentIDs.mapNotNull { MessagingModuleConfiguration.shared.messageDataProvider.getSignalAttachmentPointer(it) }
|
||||
if (!attachments.all { !it.url.isNullOrEmpty() }) {
|
||||
// Attachments
|
||||
val database = MessagingModuleConfiguration.shared.messageDataProvider
|
||||
val attachments = attachmentIDs.mapNotNull { database.getSignalAttachmentPointer(it) }
|
||||
if (attachments.any { it.url.isNullOrEmpty() }) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
//TODO equivalent to iOS's preconditionFailure
|
||||
Log.d(TAG, "Sending a message before all associated attachments have been uploaded.")
|
||||
Log.w(TAG, "Sending a message before all associated attachments have been uploaded.")
|
||||
}
|
||||
}
|
||||
val attachmentPointers = attachments.mapNotNull { Attachment.createAttachmentPointer(it) }
|
||||
dataMessage.addAllAttachments(attachmentPointers)
|
||||
// TODO Contact
|
||||
val pointers = attachments.mapNotNull { Attachment.createAttachmentPointer(it) }
|
||||
dataMessage.addAllAttachments(pointers)
|
||||
// TODO: Contact
|
||||
// Expiration timer
|
||||
// TODO: We * want * expiration timer updates to be explicit. But currently Android will disable the expiration timer for a conversation
|
||||
// if it receives a message without the current expiration timer value attached to it...
|
||||
// if it receives a message without the current expiration timer value attached to it...
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val context = MessagingModuleConfiguration.shared.context
|
||||
val expiration = if (storage.isClosedGroup(recipient!!)) Recipient.from(context, Address.fromSerialized(GroupUtil.doubleEncodeGroupID(recipient!!)), false).expireMessages
|
||||
else Recipient.from(context, Address.fromSerialized(recipient!!), false).expireMessages
|
||||
val expiration = if (storage.isClosedGroup(recipient!!)) {
|
||||
Recipient.from(context, Address.fromSerialized(GroupUtil.doubleEncodeGroupID(recipient!!)), false).expireMessages
|
||||
} else {
|
||||
Recipient.from(context, Address.fromSerialized(recipient!!), false).expireMessages
|
||||
}
|
||||
dataMessage.expireTimer = expiration
|
||||
// Group context
|
||||
if (storage.isClosedGroup(recipient!!)) {
|
||||
try {
|
||||
setGroupContext(dataMessage)
|
||||
} catch(e: Exception) {
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Couldn't construct visible message proto from: $this")
|
||||
return null
|
||||
}
|
||||
@ -135,4 +128,17 @@ class VisibleMessage : Message() {
|
||||
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 kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import nl.komponents.kovenant.Kovenant
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.functional.bind
|
||||
import nl.komponents.kovenant.functional.map
|
||||
@ -14,7 +13,6 @@ import okhttp3.HttpUrl
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.RequestBody
|
||||
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.utilities.AESGCM
|
||||
import org.session.libsignal.service.loki.HTTP
|
||||
@ -29,62 +27,38 @@ import org.whispersystems.curve25519.Curve25519
|
||||
import java.util.*
|
||||
|
||||
object OpenGroupAPIV2 {
|
||||
|
||||
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 const val DEFAULT_SERVER_PUBLIC_KEY = "a03c383cf63c3c4efe67acc52112a6dd734b3a946b9545f488aaa93da7991238"
|
||||
|
||||
private val curve = Curve25519.getInstance(Curve25519.BEST)
|
||||
val defaultRooms = MutableSharedFlow<List<DefaultGroup>>(replay = 1)
|
||||
|
||||
private val curve = Curve25519.getInstance(Curve25519.BEST)
|
||||
|
||||
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."
|
||||
}
|
||||
private const val DEFAULT_SERVER_PUBLIC_KEY = "a03c383cf63c3c4efe67acc52112a6dd734b3a946b9545f488aaa93da7991238"
|
||||
const val DEFAULT_SERVER = "http://116.203.70.33"
|
||||
|
||||
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,
|
||||
val name: String,
|
||||
val image: ByteArray?) {
|
||||
fun toJoinUrl(): String = "$DEFAULT_SERVER/$id?public_key=$DEFAULT_SERVER_PUBLIC_KEY"
|
||||
data class DefaultGroup(val id: String, val name: String, val image: ByteArray?) {
|
||||
|
||||
val joinURL: String get() = "$DEFAULT_SERVER/$id?public_key=$DEFAULT_SERVER_PUBLIC_KEY"
|
||||
}
|
||||
|
||||
data class Info(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val imageID: String?
|
||||
)
|
||||
data class Info(val id: String, val name: String, val imageID: String?)
|
||||
|
||||
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy::class)
|
||||
data class CompactPollRequest(val roomId: String,
|
||||
val authToken: String,
|
||||
val fromDeletionServerId: Long?,
|
||||
val fromMessageServerId: Long?
|
||||
)
|
||||
|
||||
data class CompactPollResult(val messages: List<OpenGroupMessageV2>,
|
||||
val deletions: List<Long>,
|
||||
val moderators: List<String>
|
||||
)
|
||||
data class CompactPollRequest(val roomID: String, val authToken: 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)
|
||||
data class MessageDeletion @JvmOverloads constructor(val id: Long = 0,
|
||||
val deletedMessageId: Long = 0
|
||||
data class MessageDeletion
|
||||
@JvmOverloads constructor(val id: Long = 0, val deletedMessageId: Long = 0
|
||||
) {
|
||||
|
||||
companion object {
|
||||
val EMPTY = MessageDeletion()
|
||||
}
|
||||
@ -99,38 +73,37 @@ object OpenGroupAPIV2 {
|
||||
val parameters: Any? = null,
|
||||
val headers: Map<String, String> = mapOf(),
|
||||
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
|
||||
* this when running over Lokinet.
|
||||
*/
|
||||
val useOnionRouting: Boolean = true
|
||||
)
|
||||
|
||||
private fun createBody(parameters: Any?): RequestBody? {
|
||||
if (parameters == null) return null
|
||||
|
||||
val parametersAsJSON = JsonUtil.toJson(parameters)
|
||||
return RequestBody.create(MediaType.get("application/json"), parametersAsJSON)
|
||||
}
|
||||
|
||||
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()
|
||||
.scheme(parsed.scheme())
|
||||
.host(parsed.host())
|
||||
.port(parsed.port())
|
||||
.addPathSegments(request.endpoint)
|
||||
|
||||
.scheme(url.scheme())
|
||||
.host(url.host())
|
||||
.port(url.port())
|
||||
.addPathSegments(request.endpoint)
|
||||
if (request.verb == GET) {
|
||||
for ((key, value) in request.queryParameters) {
|
||||
urlBuilder.addQueryParameter(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
fun execute(token: String?): Promise<Map<*, *>, Exception> {
|
||||
val requestBuilder = okhttp3.Request.Builder()
|
||||
.url(urlBuilder.build())
|
||||
.headers(Headers.of(request.headers))
|
||||
.url(urlBuilder.build())
|
||||
.headers(Headers.of(request.headers))
|
||||
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)
|
||||
}
|
||||
when (request.verb) {
|
||||
@ -139,25 +112,25 @@ object OpenGroupAPIV2 {
|
||||
POST -> requestBuilder.post(createBody(request.parameters)!!)
|
||||
DELETE -> requestBuilder.delete(createBody(request.parameters))
|
||||
}
|
||||
|
||||
if (!request.room.isNullOrEmpty()) {
|
||||
requestBuilder.header("Room", request.room)
|
||||
}
|
||||
|
||||
if (request.useOnionRouting) {
|
||||
val publicKey = MessagingModuleConfiguration.shared.storage.getOpenGroupPublicKey(request.server)
|
||||
?: return Promise.ofFail(Error.NO_PUBLIC_KEY)
|
||||
return OnionRequestAPI.sendOnionRequest(requestBuilder.build(), request.server, publicKey, isJSONRequired = isJsonRequired)
|
||||
.fail { e ->
|
||||
if (e is OnionRequestAPI.HTTPRequestFailedAtDestinationException && e.statusCode == 401) {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
if (request.room != null) {
|
||||
storage.removeAuthToken("${request.server}.${request.room}")
|
||||
} else {
|
||||
storage.removeAuthToken(request.server)
|
||||
}
|
||||
}
|
||||
?: return Promise.ofFail(Error.NoPublicKey)
|
||||
return OnionRequestAPI.sendOnionRequest(requestBuilder.build(), request.server, publicKey, isJSONRequired = isJsonRequired).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
|
||||
// indication that the token we're using has expired. Note that a 403 has a different meaning; it means that
|
||||
// we provided a valid token but it doesn't have a high enough permission level for the route in question.
|
||||
if (e is OnionRequestAPI.HTTPRequestFailedAtDestinationException && e.statusCode == 401) {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
if (request.room != null) {
|
||||
storage.removeAuthToken("${request.server}.${request.room}")
|
||||
} else {
|
||||
storage.removeAuthToken(request.server)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
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> {
|
||||
val request = Request(verb = GET, room = roomID, server = server, endpoint = "rooms/$roomID/image", isAuthRequired = false)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// region Authorization
|
||||
fun getAuthToken(room: String, server: String): Promise<String, Exception> {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
return storage.getAuthToken(room, server)?.let {
|
||||
Promise.of(it)
|
||||
} ?: run {
|
||||
requestNewAuthToken(room, server)
|
||||
.bind { claimAuthToken(it, room, server) }
|
||||
.success { authToken ->
|
||||
storage.setAuthToken(room, server, authToken)
|
||||
}
|
||||
.bind { claimAuthToken(it, room, server) }
|
||||
.success { authToken ->
|
||||
storage.setAuthToken(room, server, authToken)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun requestNewAuthToken(room: String, server: String): Promise<String, Exception> {
|
||||
val (publicKey, privateKey) = MessagingModuleConfiguration.shared.storage.getUserKeyPair()
|
||||
?: return Promise.ofFail(Error.GENERIC)
|
||||
val queryParameters = mutableMapOf("public_key" to publicKey)
|
||||
?: return Promise.ofFail(Error.Generic)
|
||||
val queryParameters = mutableMapOf( "public_key" to publicKey )
|
||||
val request = Request(GET, room, server, "auth_token_challenge", queryParameters, isAuthRequired = false, parameters = null)
|
||||
return send(request).map { json ->
|
||||
val challenge = json["challenge"] as? Map<*, *> ?: throw Error.PARSING_FAILED
|
||||
val base64EncodedCiphertext = challenge["ciphertext"] as? String
|
||||
?: throw Error.PARSING_FAILED
|
||||
val base64EncodedEphemeralPublicKey = challenge["ephemeral_public_key"] as? String
|
||||
?: throw Error.PARSING_FAILED
|
||||
val challenge = json["challenge"] as? Map<*, *> ?: throw Error.ParsingFailed
|
||||
val base64EncodedCiphertext = challenge["ciphertext"] as? String ?: throw Error.ParsingFailed
|
||||
val base64EncodedEphemeralPublicKey = challenge["ephemeral_public_key"] as? String ?: throw Error.ParsingFailed
|
||||
val ciphertext = decode(base64EncodedCiphertext)
|
||||
val ephemeralPublicKey = decode(base64EncodedEphemeralPublicKey)
|
||||
val symmetricKey = AESGCM.generateSymmetricKey(ephemeralPublicKey, privateKey)
|
||||
val tokenAsData = try {
|
||||
AESGCM.decrypt(ciphertext, symmetricKey)
|
||||
} catch (e: Exception) {
|
||||
throw Error.DECRYPTION_FAILED
|
||||
throw Error.DecryptionFailed
|
||||
}
|
||||
tokenAsData.toHexString()
|
||||
}
|
||||
}
|
||||
|
||||
fun claimAuthToken(authToken: String, room: String, server: String): Promise<String, Exception> {
|
||||
val parameters = mapOf("public_key" to MessagingModuleConfiguration.shared.storage.getUserPublicKey()!!)
|
||||
val headers = mapOf("Authorization" to authToken)
|
||||
val parameters = mapOf( "public_key" to MessagingModuleConfiguration.shared.storage.getUserPublicKey()!! )
|
||||
val headers = mapOf( "Authorization" to authToken )
|
||||
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 }
|
||||
}
|
||||
|
||||
@ -227,33 +199,36 @@ object OpenGroupAPIV2 {
|
||||
MessagingModuleConfiguration.shared.storage.removeAuthToken(room, server)
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Sending
|
||||
// region Upload/Download
|
||||
fun upload(file: ByteArray, room: String, server: String): Promise<Long, Exception> {
|
||||
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)
|
||||
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> {
|
||||
val request = Request(verb = GET, room = room, server = server, endpoint = "files/$file")
|
||||
return send(request).map { json ->
|
||||
val base64EncodedFile = json["result"] as? String ?: throw Error.PARSING_FAILED
|
||||
decode(base64EncodedFile) ?: throw Error.PARSING_FAILED
|
||||
val base64EncodedFile = json["result"] as? String ?: throw Error.ParsingFailed
|
||||
decode(base64EncodedFile) ?: throw Error.ParsingFailed
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Sending
|
||||
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 request = Request(verb = POST, room = room, server = server, endpoint = "messages", parameters = jsonMessage)
|
||||
return send(request).map { json ->
|
||||
@Suppress("UNCHECKED_CAST") val rawMessage = json["message"] as? Map<String, Any>
|
||||
?: throw Error.PARSING_FAILED
|
||||
OpenGroupMessageV2.fromJSON(rawMessage) ?: throw Error.PARSING_FAILED
|
||||
?: throw Error.ParsingFailed
|
||||
OpenGroupMessageV2.fromJSON(rawMessage) ?: throw Error.ParsingFailed
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
@ -268,10 +243,9 @@ object OpenGroupAPIV2 {
|
||||
val request = Request(verb = GET, room = room, server = server, endpoint = "messages", queryParameters = queryParameters)
|
||||
return send(request).map { jsonList ->
|
||||
@Suppress("UNCHECKED_CAST") val rawMessages = jsonList["messages"] as? List<Map<String, Any>>
|
||||
?: throw Error.PARSING_FAILED
|
||||
val lastMessageServerId = storage.getLastMessageServerId(room, server) ?: 0
|
||||
|
||||
var currentMax = lastMessageServerId
|
||||
?: throw Error.ParsingFailed
|
||||
val lastMessageServerID = storage.getLastMessageServerId(room, server) ?: 0
|
||||
var currentLastMessageServerID = lastMessageServerID
|
||||
val messages = rawMessages.mapNotNull { json ->
|
||||
try {
|
||||
val message = OpenGroupMessageV2.fromJSON(json) ?: return@mapNotNull null
|
||||
@ -285,15 +259,15 @@ object OpenGroupAPIV2 {
|
||||
Log.d("Loki", "Ignoring message with invalid signature")
|
||||
return@mapNotNull null
|
||||
}
|
||||
if (message.serverID > lastMessageServerId) {
|
||||
currentMax = message.serverID
|
||||
if (message.serverID > lastMessageServerID) {
|
||||
currentLastMessageServerID = message.serverID
|
||||
}
|
||||
message
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
storage.setLastMessageServerId(room, server, currentMax)
|
||||
storage.setLastMessageServerId(room, server, currentLastMessageServerID)
|
||||
messages
|
||||
}
|
||||
}
|
||||
@ -304,7 +278,7 @@ object OpenGroupAPIV2 {
|
||||
fun deleteMessage(serverID: Long, room: String, server: String): Promise<Unit, Exception> {
|
||||
val request = Request(verb = DELETE, room = room, server = server, endpoint = "messages/$serverID")
|
||||
return send(request).map {
|
||||
Log.d("Loki", "Deleted server message")
|
||||
Log.d("Loki", "Message deletion successful.")
|
||||
}
|
||||
}
|
||||
|
||||
@ -318,7 +292,7 @@ object OpenGroupAPIV2 {
|
||||
return send(request).map { json ->
|
||||
val type = TypeFactory.defaultInstance().constructCollectionType(List::class.java, MessageDeletion::class.java)
|
||||
val idsAsString = JsonUtil.toJson(json["ids"])
|
||||
val serverIDs = JsonUtil.fromJson<List<MessageDeletion>>(idsAsString, type) ?: throw Error.PARSING_FAILED
|
||||
val serverIDs = JsonUtil.fromJson<List<MessageDeletion>>(idsAsString, type) ?: throw Error.ParsingFailed
|
||||
val lastMessageServerId = storage.getLastDeletionServerId(room, server) ?: 0
|
||||
val serverID = serverIDs.maxByOrNull {it.id } ?: MessageDeletion.EMPTY
|
||||
if (serverID.id > lastMessageServerId) {
|
||||
@ -338,7 +312,7 @@ object OpenGroupAPIV2 {
|
||||
val request = Request(verb = GET, room = room, server = server, endpoint = "moderators")
|
||||
return send(request).map { json ->
|
||||
@Suppress("UNCHECKED_CAST") val moderatorsJson = json["moderators"] as? List<String>
|
||||
?: throw Error.PARSING_FAILED
|
||||
?: throw Error.ParsingFailed
|
||||
val id = "$server.$room"
|
||||
handleModerators(id, moderatorsJson)
|
||||
moderatorsJson
|
||||
@ -347,90 +321,86 @@ object OpenGroupAPIV2 {
|
||||
|
||||
@JvmStatic
|
||||
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)
|
||||
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> {
|
||||
val request = Request(verb = DELETE, room = room, server = server, endpoint = "block_list/$publicKey")
|
||||
return send(request).map {
|
||||
Log.d("Loki", "Unbanned user $publicKey from $server.$room")
|
||||
Log.d("Loki", "Unbanned user: $publicKey from: $server.$room")
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun isUserModerator(publicKey: String, room: String, server: String): Boolean =
|
||||
moderators["$server.$room"]?.contains(publicKey) ?: false
|
||||
moderators["$server.$room"]?.contains(publicKey) ?: false
|
||||
// endregion
|
||||
|
||||
// region General
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
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 requests = rooms.mapNotNull { room ->
|
||||
val authToken = try {
|
||||
requestAuths[room]?.get()
|
||||
authTokenRequests[room]?.get()
|
||||
} 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
|
||||
} ?: return@mapNotNull null
|
||||
|
||||
CompactPollRequest(roomId = room,
|
||||
authToken = authToken,
|
||||
fromDeletionServerId = storage.getLastDeletionServerId(room, server),
|
||||
fromMessageServerId = storage.getLastMessageServerId(room, server)
|
||||
CompactPollRequest(
|
||||
roomID = room,
|
||||
authToken = authToken,
|
||||
fromDeletionServerID = storage.getLastDeletionServerId(room, server),
|
||||
fromMessageServerID = storage.getLastMessageServerId(room, server)
|
||||
)
|
||||
}
|
||||
val request = Request(verb = POST, room = null, server = server, endpoint = "compact_poll", isAuthRequired = false, parameters = mapOf("requests" to requests))
|
||||
// build a request for all rooms
|
||||
val request = Request(verb = POST, room = null, server = server, endpoint = "compact_poll", isAuthRequired = false, parameters = mapOf( "requests" to requests ))
|
||||
return send(request = request).map { json ->
|
||||
val results = json["results"] as? List<*> ?: throw Error.PARSING_FAILED
|
||||
|
||||
results.mapNotNull { roomJson ->
|
||||
if (roomJson !is Map<*,*>) return@mapNotNull null
|
||||
val roomId = roomJson["room_id"] as? String ?: return@mapNotNull null
|
||||
|
||||
// check the status was fine
|
||||
val statusCode = roomJson["status_code"] as? Int ?: return@mapNotNull null
|
||||
val results = json["results"] as? List<*> ?: throw Error.ParsingFailed
|
||||
results.mapNotNull { json ->
|
||||
if (json !is Map<*,*>) return@mapNotNull null
|
||||
val roomID = json["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
|
||||
// we provided a valid token but it doesn't have a high enough permission level for the route in question.
|
||||
val statusCode = json["status_code"] as? Int ?: return@mapNotNull null
|
||||
if (statusCode == 401) {
|
||||
// delete auth token and return null
|
||||
storage.removeAuthToken(roomId, server)
|
||||
storage.removeAuthToken(roomID, server)
|
||||
}
|
||||
|
||||
// check and store mods
|
||||
val moderators = roomJson["moderators"] as? List<String> ?: return@mapNotNull null
|
||||
handleModerators("$server.$roomId", moderators)
|
||||
|
||||
// get deletions
|
||||
// Moderators
|
||||
val moderators = json["moderators"] as? List<String> ?: return@mapNotNull null
|
||||
handleModerators("$server.$roomID", moderators)
|
||||
// Deletions
|
||||
val type = TypeFactory.defaultInstance().constructCollectionType(List::class.java, MessageDeletion::class.java)
|
||||
val idsAsString = JsonUtil.toJson(roomJson["deletions"])
|
||||
val deletedServerIDs = JsonUtil.fromJson<List<MessageDeletion>>(idsAsString, type) ?: throw Error.PARSING_FAILED
|
||||
val lastDeletionServerId = storage.getLastDeletionServerId(roomId, server) ?: 0
|
||||
val idsAsString = JsonUtil.toJson(json["deletions"])
|
||||
val deletedServerIDs = JsonUtil.fromJson<List<MessageDeletion>>(idsAsString, type) ?: throw Error.ParsingFailed
|
||||
val lastDeletionServerID = storage.getLastDeletionServerId(roomID, server) ?: 0
|
||||
val serverID = deletedServerIDs.maxByOrNull {it.id } ?: MessageDeletion.EMPTY
|
||||
if (serverID.id > lastDeletionServerId) {
|
||||
storage.setLastDeletionServerId(roomId, server, serverID.id)
|
||||
if (serverID.id > lastDeletionServerID) {
|
||||
storage.setLastDeletionServerId(roomID, server, serverID.id)
|
||||
}
|
||||
|
||||
// get messages
|
||||
val rawMessages = roomJson["messages"] as? List<Map<String, Any>> ?: return@mapNotNull null // parsing failed
|
||||
|
||||
val lastMessageServerId = storage.getLastMessageServerId(roomId, server) ?: 0
|
||||
var currentMax = lastMessageServerId
|
||||
// Messages
|
||||
val rawMessages = json["messages"] as? List<Map<String, Any>> ?: return@mapNotNull null
|
||||
val lastMessageServerID = storage.getLastMessageServerId(roomID, server) ?: 0
|
||||
var currentLastMessageServerID = lastMessageServerID
|
||||
val messages = rawMessages.mapNotNull { rawMessage ->
|
||||
val message = OpenGroupMessageV2.fromJSON(rawMessage)?.apply {
|
||||
currentMax = maxOf(currentMax,this.serverID ?: 0)
|
||||
currentLastMessageServerID = maxOf(currentLastMessageServerID,this.serverID ?: 0)
|
||||
}
|
||||
// TODO: We need to check the signature here...
|
||||
message
|
||||
}
|
||||
storage.setLastMessageServerId(roomId, server, currentMax)
|
||||
roomId to CompactPollResult(
|
||||
messages = messages,
|
||||
deletions = deletedServerIDs.map { it.deletedMessageId },
|
||||
moderators = moderators
|
||||
storage.setLastMessageServerId(roomID, server, currentLastMessageServerID)
|
||||
roomID to CompactPollResult(
|
||||
messages = messages,
|
||||
deletions = deletedServerIDs.map { it.deletedMessageId },
|
||||
moderators = moderators
|
||||
)
|
||||
}.toMap()
|
||||
}
|
||||
@ -443,7 +413,7 @@ object OpenGroupAPIV2 {
|
||||
val earlyGroups = groups.map { group ->
|
||||
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 ->
|
||||
if (replayed.none { it.image?.isNotEmpty() == true}) {
|
||||
defaultRooms.tryEmit(earlyGroups)
|
||||
@ -452,12 +422,11 @@ object OpenGroupAPIV2 {
|
||||
val images = groups.map { group ->
|
||||
group.id to downloadOpenGroupProfilePicture(group.id, DEFAULT_SERVER)
|
||||
}.toMap()
|
||||
|
||||
groups.map { group ->
|
||||
val image = try {
|
||||
images[group.id]!!.get()
|
||||
} catch (e: Exception) {
|
||||
// no image or image failed to download
|
||||
// No image or image failed to download
|
||||
null
|
||||
}
|
||||
DefaultGroup(group.id, group.name, image)
|
||||
@ -470,9 +439,9 @@ object OpenGroupAPIV2 {
|
||||
fun getInfo(room: String, server: String): Promise<Info, Exception> {
|
||||
val request = Request(verb = GET, room = null, server = server, endpoint = "rooms/$room", isAuthRequired = false)
|
||||
return send(request).map { json ->
|
||||
val rawRoom = json["room"] as? Map<*, *> ?: throw Error.PARSING_FAILED
|
||||
val id = rawRoom["id"] as? String ?: throw Error.PARSING_FAILED
|
||||
val name = rawRoom["name"] as? String ?: throw Error.PARSING_FAILED
|
||||
val rawRoom = json["room"] as? Map<*, *> ?: throw Error.ParsingFailed
|
||||
val id = rawRoom["id"] as? String ?: throw Error.ParsingFailed
|
||||
val name = rawRoom["name"] as? String ?: throw Error.ParsingFailed
|
||||
val imageID = rawRoom["image_id"] as? String
|
||||
Info(id = id, name = name, imageID = imageID)
|
||||
}
|
||||
@ -481,13 +450,13 @@ object OpenGroupAPIV2 {
|
||||
fun getAllRooms(server: String): Promise<List<Info>, Exception> {
|
||||
val request = Request(verb = GET, room = null, server = server, endpoint = "rooms", isAuthRequired = false)
|
||||
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 {
|
||||
val roomJson = it as? Map<*, *> ?: return@mapNotNull null
|
||||
val id = roomJson["id"] as? String ?: return@mapNotNull null
|
||||
val name = roomJson["name"] as? String ?: return@mapNotNull null
|
||||
val imageId = roomJson["image_id"] as? String
|
||||
Info(id, name, imageId)
|
||||
val imageID = roomJson["image_id"] as? String
|
||||
Info(id, name, imageID)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -495,12 +464,11 @@ object OpenGroupAPIV2 {
|
||||
fun getMemberCount(room: String, server: String): Promise<Int, Exception> {
|
||||
val request = Request(verb = GET, room = room, server = server, endpoint = "member_count")
|
||||
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
|
||||
storage.setUserCount(room, server, memberCount)
|
||||
memberCount
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
||||
}
|
@ -9,14 +9,18 @@ import org.session.libsignal.utilities.logging.Log
|
||||
import org.whispersystems.curve25519.Curve25519
|
||||
|
||||
data class OpenGroupMessageV2(
|
||||
val serverID: Long? = null,
|
||||
val sender: String?,
|
||||
val sentTimestamp: Long,
|
||||
// The serialized protobuf in base64 encoding
|
||||
val base64EncodedData: String,
|
||||
// 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
|
||||
val serverID: Long? = null,
|
||||
val sender: String?,
|
||||
val sentTimestamp: Long,
|
||||
/**
|
||||
* The serialized protobuf in base64 encoding.
|
||||
*/
|
||||
val base64EncodedData: String,
|
||||
/**
|
||||
* 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 {
|
||||
@ -28,11 +32,12 @@ data class OpenGroupMessageV2(
|
||||
val serverID = json["server_id"] as? Int
|
||||
val sender = json["public_key"] as? String
|
||||
val base64EncodedSignature = json["signature"] as? String
|
||||
return OpenGroupMessageV2(serverID = serverID?.toLong(),
|
||||
sender = sender,
|
||||
sentTimestamp = sentTimestamp,
|
||||
base64EncodedData = base64EncodedData,
|
||||
base64EncodedSignature = base64EncodedSignature
|
||||
return OpenGroupMessageV2(
|
||||
serverID = serverID?.toLong(),
|
||||
sender = sender,
|
||||
sentTimestamp = sentTimestamp,
|
||||
base64EncodedData = base64EncodedData,
|
||||
base64EncodedSignature = base64EncodedSignature
|
||||
)
|
||||
}
|
||||
|
||||
@ -41,29 +46,26 @@ data class OpenGroupMessageV2(
|
||||
fun sign(): OpenGroupMessageV2? {
|
||||
if (base64EncodedData.isEmpty()) return null
|
||||
val (publicKey, privateKey) = MessagingModuleConfiguration.shared.storage.getUserKeyPair() ?: return null
|
||||
|
||||
if (sender != publicKey) return null // only sign our own messages?
|
||||
|
||||
if (sender != publicKey) return null
|
||||
val signature = try {
|
||||
curve.calculateSignature(privateKey, decode(base64EncodedData))
|
||||
} catch (e: Exception) {
|
||||
Log.e("Loki", "Couldn't sign OpenGroupV2Message", e)
|
||||
Log.w("Loki", "Couldn't sign open group message.", e)
|
||||
return null
|
||||
}
|
||||
|
||||
return copy(base64EncodedSignature = Base64.encodeBytes(signature))
|
||||
}
|
||||
|
||||
fun toJSON(): Map<String, Any> {
|
||||
val jsonMap = mutableMapOf("data" to base64EncodedData, "timestamp" to sentTimestamp)
|
||||
serverID?.let { jsonMap["server_id"] = serverID }
|
||||
sender?.let { jsonMap["public_key"] = sender }
|
||||
base64EncodedSignature?.let { jsonMap["signature"] = base64EncodedSignature }
|
||||
return jsonMap
|
||||
val json = mutableMapOf( "data" to base64EncodedData, "timestamp" to sentTimestamp )
|
||||
serverID?.let { json["server_id"] = it }
|
||||
sender?.let { json["public_key"] = it }
|
||||
base64EncodedSignature?.let { json["signature"] = it }
|
||||
return json
|
||||
}
|
||||
|
||||
fun toProto(): SignalServiceProtos.Content = decode(base64EncodedData).let(PushTransportDetails::getStrippedPaddingMessageBody).let { bytes ->
|
||||
SignalServiceProtos.Content.parseFrom(bytes)
|
||||
fun toProto(): SignalServiceProtos.Content {
|
||||
val data = decode(base64EncodedData).let(PushTransportDetails::getStrippedPaddingMessageBody)
|
||||
return SignalServiceProtos.Content.parseFrom(data)
|
||||
}
|
||||
|
||||
}
|
@ -1,51 +1,50 @@
|
||||
package org.session.libsession.messaging.open_groups
|
||||
|
||||
import org.session.libsignal.utilities.JsonUtil
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import java.util.*
|
||||
|
||||
data class OpenGroupV2(
|
||||
val server: String,
|
||||
val room: String,
|
||||
val id: String,
|
||||
val name: String,
|
||||
val publicKey: String
|
||||
val server: String,
|
||||
val room: String,
|
||||
val id: String,
|
||||
val name: String,
|
||||
val publicKey: String
|
||||
) {
|
||||
|
||||
constructor(server: String, room: String, name: String, publicKey: String) : this(
|
||||
server = server,
|
||||
room = room,
|
||||
id = "$server.$room",
|
||||
name = name,
|
||||
publicKey = publicKey,
|
||||
server = server,
|
||||
room = room,
|
||||
id = "$server.$room",
|
||||
name = name,
|
||||
publicKey = publicKey,
|
||||
)
|
||||
|
||||
companion object {
|
||||
|
||||
fun fromJson(jsonAsString: String): OpenGroupV2? {
|
||||
fun fromJSON(jsonAsString: String): OpenGroupV2? {
|
||||
return try {
|
||||
val json = JsonUtil.fromJson(jsonAsString)
|
||||
if (!json.has("room")) return null
|
||||
|
||||
val room = json.get("room").asText().toLowerCase(Locale.getDefault())
|
||||
val server = json.get("server").asText().toLowerCase(Locale.getDefault())
|
||||
val room = json.get("room").asText().toLowerCase(Locale.US)
|
||||
val server = json.get("server").asText().toLowerCase(Locale.US)
|
||||
val displayName = json.get("displayName").asText()
|
||||
val publicKey = json.get("publicKey").asText()
|
||||
|
||||
OpenGroupV2(server, room, displayName, publicKey)
|
||||
} catch (e: Exception) {
|
||||
Log.w("Loki", "Couldn't parse open group from JSON: $jsonAsString.", e);
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun toJoinUrl(): String = "$server/$room?public_key=$publicKey"
|
||||
|
||||
fun toJson(): Map<String,String> = mapOf(
|
||||
"room" to room,
|
||||
"server" to server,
|
||||
"displayName" to name,
|
||||
"publicKey" to publicKey,
|
||||
"room" to room,
|
||||
"server" to server,
|
||||
"displayName" to name,
|
||||
"publicKey" to publicKey,
|
||||
)
|
||||
|
||||
val joinURL: String get() = "$server/$room?public_key=$publicKey"
|
||||
}
|
@ -126,7 +126,7 @@ private fun handleConfigurationMessage(message: ConfigurationMessage) {
|
||||
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 allV2OpenGroups = storage.getAllV2OpenGroups().map { it.value.toJoinUrl() }
|
||||
val allV2OpenGroups = storage.getAllV2OpenGroups().map { it.value.joinURL }
|
||||
for (openGroup in message.openGroups) {
|
||||
if (allOpenGroups.contains(openGroup) || allV2OpenGroups.contains(openGroup)) continue
|
||||
storage.addOpenGroup(openGroup, 1)
|
||||
|
Loading…
Reference in New Issue
Block a user