feat: adding default group handling to frontend viewmodel

This commit is contained in:
jubb 2021-04-20 17:22:36 +10:00
parent aea23a6fc1
commit 1e164f8648
15 changed files with 246 additions and 185 deletions

View file

@ -9,8 +9,7 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.Toast import android.widget.Toast
import androidx.fragment.app.Fragment import androidx.fragment.app.*
import androidx.fragment.app.FragmentPagerAdapter
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import kotlinx.android.synthetic.main.activity_join_public_chat.* import kotlinx.android.synthetic.main.activity_join_public_chat.*
import kotlinx.android.synthetic.main.fragment_enter_chat_url.* import kotlinx.android.synthetic.main.fragment_enter_chat_url.*
@ -18,13 +17,16 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsignal.utilities.logging.Log
import org.thoughtcrime.securesms.BaseActionBarActivity import org.thoughtcrime.securesms.BaseActionBarActivity
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.session.libsignal.utilities.logging.Log
import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragment import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragment
import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragmentDelegate import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragmentDelegate
import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol
import org.thoughtcrime.securesms.loki.utilities.OpenGroupUtilities import org.thoughtcrime.securesms.loki.utilities.OpenGroupUtilities
import org.thoughtcrime.securesms.loki.viewmodel.DefaultGroup
import org.thoughtcrime.securesms.loki.viewmodel.DefaultGroupsViewModel
import org.thoughtcrime.securesms.loki.viewmodel.State
class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCodeWrapperFragmentDelegate { class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCodeWrapperFragmentDelegate {
private val adapter = JoinPublicChatActivityAdapter(this) private val adapter = JoinPublicChatActivityAdapter(this)
@ -122,14 +124,34 @@ private class JoinPublicChatActivityAdapter(val activity: JoinPublicChatActivity
// region Enter Chat URL Fragment // region Enter Chat URL Fragment
class EnterChatURLFragment : Fragment() { class EnterChatURLFragment : Fragment() {
// factory producer is app scoped because default groups will want to stick around for app lifetime
private val viewModel by activityViewModels<DefaultGroupsViewModel>()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return inflater.inflate(R.layout.fragment_enter_chat_url, container, false) return inflater.inflate(R.layout.fragment_enter_chat_url, container, false)
} }
private fun populateDefaultGroups(groups: List<DefaultGroup>) {
Log.d("Loki", "Got some default groups $groups")
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
chatURLEditText.imeOptions = chatURLEditText.imeOptions or 16777216 // Always use incognito keyboard chatURLEditText.imeOptions = chatURLEditText.imeOptions or 16777216 // Always use incognito keyboard
joinPublicChatButton.setOnClickListener { joinPublicChatIfPossible() } joinPublicChatButton.setOnClickListener { joinPublicChatIfPossible() }
viewModel.defaultRooms.observe(viewLifecycleOwner) { state ->
when (state) {
State.Loading -> {
// show a loader here probs
}
is State.Error -> {
// hide the loader and the
}
is State.Success -> {
populateDefaultGroups(state.value)
}
}
}
} }
private fun joinPublicChatIfPossible() { private fun joinPublicChatIfPossible() {

View file

@ -30,7 +30,7 @@ class PublicChatManager(private val context: Context) {
public fun areAllCaughtUp(): Boolean { public fun areAllCaughtUp(): Boolean {
var areAllCaughtUp = true var areAllCaughtUp = true
refreshChatsAndPollers() refreshChatsAndPollers()
for ((threadID, chat) in chats) { for ((threadID, _) in chats) {
val poller = pollers[threadID] val poller = pollers[threadID]
areAllCaughtUp = if (poller != null) areAllCaughtUp && poller.isCaughtUp else true areAllCaughtUp = if (poller != null) areAllCaughtUp && poller.isCaughtUp else true
} }
@ -83,9 +83,9 @@ class PublicChatManager(private val context: Context) {
@WorkerThread @WorkerThread
fun addChat(server: String, room: String): OpenGroupV2 { fun addChat(server: String, room: String): OpenGroupV2 {
// Ensure the auth token is acquired. // Ensure the auth token is acquired.
OpenGroupAPIV2.getAuthToken(server).get() OpenGroupAPIV2.getAuthToken(room, server).get()
val channelInfo = OpenGroupAPIV2.getChannelInfo(channel, server).get() val channelInfo = OpenGroupAPIV2.getInfo(room, server).get()
return addChat(server, room, channelInfo) return addChat(server, room, channelInfo)
} }
@ -116,17 +116,19 @@ class PublicChatManager(private val context: Context) {
} }
@WorkerThread @WorkerThread
fun addChat(server: String, room: String, info: OpenGroupInfo): OpenGroupV2 { fun addChat(server: String, room: String, info: OpenGroupAPIV2.Info): OpenGroupV2 {
val chat = OpenGroupV2(server, room, info.displayName, ) val chat = OpenGroupV2(server, room, info.id, info.name, info.imageID)
var threadID = GroupManager.getOpenGroupThreadID(chat.id, context) val threadID = GroupManager.getOpenGroupThreadID(chat.id, context)
var profilePicture: Bitmap? = null var profilePicture: Bitmap? = null
if (threadID < 0) { if (threadID < 0) {
if (info.profilePictureURL.isNotEmpty()) { val imageID = info.imageID
val profilePictureAsByteArray = OpenGroupAPIV2.downloadOpenGroupProfilePicture(server, info.profilePictureURL) if (!imageID.isNullOrEmpty()) {
val profilePictureAsByteArray = OpenGroupAPIV2.downloadOpenGroupProfilePicture(imageID)
profilePicture = BitmapUtil.fromByteArray(profilePictureAsByteArray) profilePicture = BitmapUtil.fromByteArray(profilePictureAsByteArray)
} }
val result = GroupManager.createOpenGroup() val result = GroupManager.createOpenGroup(chat.id, context, profilePicture, info.name)
} }
return chat
} }
public fun removeChat(server: String, channel: Long) { public fun removeChat(server: String, channel: Long) {

View file

@ -0,0 +1,39 @@
package org.thoughtcrime.securesms.loki.viewmodel
import androidx.lifecycle.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import org.session.libsession.messaging.opengroups.OpenGroupAPIV2
import org.session.libsignal.utilities.logging.Log
class DefaultGroupsViewModel : ViewModel() {
init {
OpenGroupAPIV2.getDefaultRoomsIfNeeded()
}
val defaultRooms = OpenGroupAPIV2.defaultRooms.asLiveData().distinctUntilChanged().switchMap { groups ->
liveData {
// load images etc
emit(State.Loading)
val images = groups.filterNot { it.imageID.isNullOrEmpty() }.map { group ->
val image = viewModelScope.async(Dispatchers.IO) {
try {
OpenGroupAPIV2.downloadOpenGroupProfilePicture(group.imageID!!)
} catch (e: Exception) {
Log.e("Loki", "Error getting group profile picture", e)
null
}
}
group.id to image
}.toMap()
val defaultGroups = groups.map { group ->
DefaultGroup(group.id, group.name, images[group.id]?.await())
}
emit(State.Success(defaultGroups))
}
}
}
data class DefaultGroup(val id: String, val name: String, val image: ByteArray?)

View file

@ -0,0 +1,7 @@
package org.thoughtcrime.securesms.loki.viewmodel
sealed class State<T> {
object Loading : State<Nothing>()
data class Success<T>(val value: T): State<T>()
data class Error(val error: Exception): State<Nothing>()
}

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/contentView" android:id="@+id/contentView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
@ -22,12 +22,49 @@
android:layout_marginTop="@dimen/large_spacing" android:layout_marginTop="@dimen/large_spacing"
android:layout_marginRight="@dimen/large_spacing" android:layout_marginRight="@dimen/large_spacing"
android:inputType="textWebEmailAddress" android:inputType="textWebEmailAddress"
android:hint="Enter an open group URL" /> android:hint="@string/fragment_enter_chat_url_edit_text_hint" />
<View <View
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_weight="1" /> android:layout_weight="1" />
<androidx.gridlayout.widget.GridLayout
app:columnCount="2"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.chip.Chip
android:theme="@style/Theme.MaterialComponents.DayNight"
style="?attr/chipStyle"
android:textAppearance="@style/TextAppearance.AppCompat.Large"
app:textStartPadding="10dp"
android:text="Main"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<com.google.android.material.chip.Chip
android:theme="@style/Theme.MaterialComponents.DayNight"
style="?attr/chipStyle"
android:textAppearance="@style/TextAppearance.AppCompat.Large"
app:textStartPadding="10dp"
android:text="Main"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<com.google.android.material.chip.Chip
android:theme="@style/Theme.MaterialComponents.DayNight"
style="?attr/chipStyle"
android:textAppearance="@style/TextAppearance.AppCompat.Large"
app:textStartPadding="10dp"
android:text="Main"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<com.google.android.material.chip.Chip
android:theme="@style/Theme.MaterialComponents.DayNight"
style="?attr/chipStyle"
android:textAppearance="@style/TextAppearance.AppCompat.Large"
app:textStartPadding="10dp"
android:text="Main"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</androidx.gridlayout.widget.GridLayout>
<Button <Button
style="@style/Widget.Session.Button.Common.ProminentOutline" style="@style/Widget.Session.Button.Common.ProminentOutline"

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/contentView" android:id="@+id/contentView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
@ -29,6 +29,14 @@
android:layout_height="0dp" android:layout_height="0dp"
android:layout_weight="1" /> android:layout_weight="1" />
<com.google.android.material.chip.Chip
android:theme="@style/Theme.MaterialComponents.DayNight"
app:closeIconEnabled="true"
style="?attr/chipStyle"
android:text="Main"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<Button <Button
style="@style/Widget.Session.Button.Common.ProminentOutline" style="@style/Widget.Session.Button.Common.ProminentOutline"
android:id="@+id/joinPublicChatButton" android:id="@+id/joinPublicChatButton"

View file

@ -3,7 +3,7 @@
<domain-config cleartextTrafficPermitted="true"> <domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">127.0.0.1</domain> <domain includeSubdomains="true">127.0.0.1</domain>
</domain-config> </domain-config>
<domain-config cleartextTrafficPermitted="false"> <domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="false">public.loki.foundation</domain> <domain includeSubdomains="false">public.loki.foundation</domain>
<trust-anchors> <trust-anchors>
<certificates src="@raw/lf_session_cert"/> <certificates src="@raw/lf_session_cert"/>
@ -21,4 +21,13 @@
<certificates src="@raw/seed3cert"/> <certificates src="@raw/seed3cert"/>
</trust-anchors> </trust-anchors>
</domain-config> </domain-config>
<debug-overrides>
<base-config cleartextTrafficPermitted="true">
<trust-anchors>
<!-- Trust user added CAs while debuggable only -->
<certificates src="user" />
<certificates src="system" />
</trust-anchors>
</base-config>
</debug-overrides>
</network-security-config> </network-security-config>

View file

@ -1,5 +1,7 @@
package org.session.libsession.messaging.opengroups package org.session.libsession.messaging.opengroups
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableSharedFlow
import nl.komponents.kovenant.Kovenant 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
@ -8,11 +10,13 @@ import okhttp3.HttpUrl
import okhttp3.MediaType import okhttp3.MediaType
import okhttp3.RequestBody import okhttp3.RequestBody
import org.session.libsession.messaging.MessagingConfiguration import org.session.libsession.messaging.MessagingConfiguration
import org.session.libsession.messaging.fileserver.FileServerAPI
import org.session.libsession.messaging.opengroups.OpenGroupAPIV2.Error import org.session.libsession.messaging.opengroups.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.api.utilities.HTTP import org.session.libsignal.service.loki.api.utilities.HTTP
import org.session.libsignal.service.loki.api.utilities.HTTP.Verb.* import org.session.libsignal.service.loki.api.utilities.HTTP.Verb.*
import org.session.libsignal.service.loki.utilities.DownloadUtilities
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded 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.Base64.* import org.session.libsignal.utilities.Base64.*
@ -20,13 +24,16 @@ import org.session.libsignal.utilities.JsonUtil
import org.session.libsignal.utilities.createContext import org.session.libsignal.utilities.createContext
import org.session.libsignal.utilities.logging.Log import org.session.libsignal.utilities.logging.Log
import org.whispersystems.curve25519.Curve25519 import org.whispersystems.curve25519.Curve25519
import java.io.ByteArrayOutputStream
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 = "https://sessionopengroup.com" private const val DEFAULT_SERVER = "https://sog.ibolpap.finance"
const val DEFAULT_SERVER_PUBLIC_KEY = "658d29b91892a2389505596b135e76a53db6e11d613a51dbd3d0816adffb231b" private const val DEFAULT_SERVER_PUBLIC_KEY = "b464aa186530c97d6bcf663a3a3b7465a5f782beaa67c83bee99468824b4aa10"
val defaultRooms = MutableSharedFlow<List<Info>>(replay = 1)
private val sharedContext = Kovenant.createContext() private val sharedContext = Kovenant.createContext()
private val curve = Curve25519.getInstance(Curve25519.BEST) private val curve = Curve25519.getInstance(Curve25519.BEST)
@ -65,7 +72,7 @@ object OpenGroupAPIV2 {
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(request.server) ?: return Promise.ofFail(Error.INVALID_URL) val parsed = HttpUrl.parse(request.server) ?: return Promise.ofFail(Error.INVALID_URL)
val urlBuilder = HttpUrl.Builder() val urlBuilder = HttpUrl.Builder()
.scheme(parsed.scheme()) .scheme(parsed.scheme())
@ -117,6 +124,21 @@ object OpenGroupAPIV2 {
} }
} }
fun downloadOpenGroupProfilePicture(imageUrl: String): ByteArray? {
Log.d("Loki", "Downloading open group profile picture from \"$imageUrl\".")
val outputStream = ByteArrayOutputStream()
try {
DownloadUtilities.downloadFile(outputStream, imageUrl, FileServerAPI.maxFileSize, null)
Log.d("Loki", "Open group profile picture was successfully loaded from \"$imageUrl\"")
return outputStream.toByteArray()
} catch (e: Exception) {
Log.d("Loki", "Failed to download open group profile picture from \"$imageUrl\" due to error: $e.")
return null
} finally {
outputStream.close()
}
}
fun getAuthToken(room: String, server: String): Promise<String, Exception> { fun getAuthToken(room: String, server: String): Promise<String, Exception> {
val storage = MessagingConfiguration.shared.storage val storage = MessagingConfiguration.shared.storage
return storage.getAuthToken(room, server)?.let { return storage.getAuthToken(room, server)?.let {
@ -136,9 +158,11 @@ object OpenGroupAPIV2 {
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(sharedContext) { json -> return send(request).map(sharedContext) { json ->
val challenge = json["challenge"] as? Map<*,*> ?: throw Error.PARSING_FAILED val challenge = json["challenge"] as? Map<*, *> ?: throw Error.PARSING_FAILED
val base64EncodedCiphertext = challenge["ciphertext"] as? String ?: throw Error.PARSING_FAILED val base64EncodedCiphertext = challenge["ciphertext"] as? String
val base64EncodedEphemeralPublicKey = challenge["ephemeral_public_key"] as? String ?: throw Error.PARSING_FAILED ?: throw Error.PARSING_FAILED
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)
@ -189,7 +213,8 @@ object OpenGroupAPIV2 {
val json = signedMessage.toJSON() val json = signedMessage.toJSON()
val request = Request(verb = POST, room = room, server = server, endpoint = "messages", parameters = json) val request = Request(verb = POST, room = room, server = server, endpoint = "messages", parameters = json)
return send(request).map(sharedContext) { return send(request).map(sharedContext) {
@Suppress("UNCHECKED_CAST") val rawMessage = json["message"] as? Map<String,String> ?: throw Error.PARSING_FAILED @Suppress("UNCHECKED_CAST") val rawMessage = json["message"] as? Map<String, String>
?: throw Error.PARSING_FAILED
OpenGroupMessageV2.fromJSON(rawMessage) ?: throw Error.PARSING_FAILED OpenGroupMessageV2.fromJSON(rawMessage) ?: throw Error.PARSING_FAILED
} }
} }
@ -198,13 +223,14 @@ object OpenGroupAPIV2 {
// region Messages // region Messages
fun getMessages(room: String, server: String): Promise<List<OpenGroupMessageV2>, Exception> { fun getMessages(room: String, server: String): Promise<List<OpenGroupMessageV2>, Exception> {
val storage = MessagingConfiguration.shared.storage val storage = MessagingConfiguration.shared.storage
val queryParameters = mutableMapOf<String,String>() val queryParameters = mutableMapOf<String, String>()
storage.getLastMessageServerId(room,server)?.let { lastId -> storage.getLastMessageServerId(room, server)?.let { lastId ->
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(sharedContext) { jsonList -> return send(request).map(sharedContext) { jsonList ->
@Suppress("UNCHECKED_CAST") val rawMessages = jsonList["messages"] as? List<Map<String,Any>> ?: throw Error.PARSING_FAILED @Suppress("UNCHECKED_CAST") val rawMessages = jsonList["messages"] as? List<Map<String, Any>>
?: throw Error.PARSING_FAILED
val lastMessageServerId = storage.getLastMessageServerId(room, server) ?: 0 val lastMessageServerId = storage.getLastMessageServerId(room, server) ?: 0
var currentMax = lastMessageServerId var currentMax = lastMessageServerId
@ -225,7 +251,7 @@ object OpenGroupAPIV2 {
} }
message message
} }
storage.setLastMessageServerId(room,server,currentMax) storage.setLastMessageServerId(room, server, currentMax)
messages messages
} }
} }
@ -241,13 +267,14 @@ object OpenGroupAPIV2 {
fun getDeletedMessages(room: String, server: String): Promise<List<Long>, Exception> { fun getDeletedMessages(room: String, server: String): Promise<List<Long>, Exception> {
val storage = MessagingConfiguration.shared.storage val storage = MessagingConfiguration.shared.storage
val queryParameters = mutableMapOf<String,String>() val queryParameters = mutableMapOf<String, String>()
storage.getLastDeletionServerId(room, server)?.let { last -> storage.getLastDeletionServerId(room, server)?.let { last ->
queryParameters["from_server_id"] = last.toString() queryParameters["from_server_id"] = last.toString()
} }
val request = Request(verb = GET, room = room, server = server, endpoint = "deleted_messages", queryParameters = queryParameters) val request = Request(verb = GET, room = room, server = server, endpoint = "deleted_messages", queryParameters = queryParameters)
return send(request).map(sharedContext) { json -> return send(request).map(sharedContext) { json ->
@Suppress("UNCHECKED_CAST") val serverIDs = json["ids"] as? List<Long> ?: throw Error.PARSING_FAILED @Suppress("UNCHECKED_CAST") val serverIDs = json["ids"] as? List<Long>
?: throw Error.PARSING_FAILED
val lastMessageServerId = storage.getLastMessageServerId(room, server) ?: 0 val lastMessageServerId = storage.getLastMessageServerId(room, server) ?: 0
val serverID = serverIDs.maxOrNull() ?: 0 val serverID = serverIDs.maxOrNull() ?: 0
if (serverID > lastMessageServerId) { if (serverID > lastMessageServerId) {
@ -262,7 +289,8 @@ object OpenGroupAPIV2 {
fun getModerators(room: String, server: String): Promise<List<String>, Exception> { fun getModerators(room: String, server: String): Promise<List<String>, Exception> {
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(sharedContext) { json -> return send(request).map(sharedContext) { json ->
@Suppress("UNCHECKED_CAST") val moderatorsJson = json["moderators"] as? List<String> ?: throw Error.PARSING_FAILED @Suppress("UNCHECKED_CAST") val moderatorsJson = json["moderators"] as? List<String>
?: throw Error.PARSING_FAILED
val id = "$server.$room" val id = "$server.$room"
moderators[id] = moderatorsJson.toMutableSet() moderators[id] = moderatorsJson.toMutableSet()
moderatorsJson moderatorsJson
@ -284,20 +312,23 @@ object OpenGroupAPIV2 {
} }
} }
fun isUserModerator(publicKey: String, room: String, server: String): Boolean = moderators["$server.$room"]?.contains(publicKey) ?: false fun isUserModerator(publicKey: String, room: String, server: String): Boolean = moderators["$server.$room"]?.contains(publicKey)
?: false
// endregion // endregion
// region General // region General
fun getDefaultRoomsIfNeeded(): Promise<List<Info>, Exception> { fun getDefaultRoomsIfNeeded(): Promise<List<Info>, Exception> {
val storage = MessagingConfiguration.shared.storage val storage = MessagingConfiguration.shared.storage
storage.setOpenGroupPublicKey(DEFAULT_SERVER, DEFAULT_SERVER_PUBLIC_KEY) storage.setOpenGroupPublicKey(DEFAULT_SERVER, DEFAULT_SERVER_PUBLIC_KEY)
return getAllRooms(DEFAULT_SERVER) return getAllRooms(DEFAULT_SERVER).success { new ->
defaultRooms.tryEmit(new)
}
} }
fun getInfo(room: String, server: String): Promise<Info, Exception> { fun getInfo(room: String, server: String): Promise<Info, Exception> {
val request = Request(verb = GET, room = room, server = server, endpoint = "rooms/$room", isAuthRequired = false) val request = Request(verb = GET, room = room, server = server, endpoint = "rooms/$room", isAuthRequired = false)
return send(request).map(sharedContext) { json -> return send(request).map(sharedContext) { json ->
val rawRoom = json["room"] as? Map<*,*> ?: throw Error.PARSING_FAILED val rawRoom = json["room"] as? Map<*, *> ?: throw Error.PARSING_FAILED
val id = rawRoom["id"] as? String ?: 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 name = rawRoom["name"] as? String ?: throw Error.PARSING_FAILED
val imageID = rawRoom["image_id"] as? String val imageID = rawRoom["image_id"] as? String
@ -308,7 +339,7 @@ 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(sharedContext) { json -> return send(request).map(sharedContext) { json ->
val rawRooms = json["rooms"] as? Map<*,*> ?: throw Error.PARSING_FAILED val rawRooms = json["rooms"] as? List<Map<*, *>> ?: throw Error.PARSING_FAILED
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

View file

@ -1,7 +1,9 @@
package org.session.libsession.messaging.opengroups package org.session.libsession.messaging.opengroups
import org.session.libsession.messaging.MessagingConfiguration import org.session.libsession.messaging.MessagingConfiguration
import org.session.libsignal.service.internal.push.SignalServiceProtos
import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.Base64
import org.session.libsignal.utilities.Base64.decode
import org.session.libsignal.utilities.logging.Log import org.session.libsignal.utilities.logging.Log
import org.whispersystems.curve25519.Curve25519 import org.whispersystems.curve25519.Curve25519
@ -58,4 +60,9 @@ data class OpenGroupMessageV2(
base64EncodedSignature?.let { jsonMap["signature"] = base64EncodedSignature } base64EncodedSignature?.let { jsonMap["signature"] = base64EncodedSignature }
return jsonMap return jsonMap
} }
fun toProto(): SignalServiceProtos.DataMessage = decode(base64EncodedData).let { bytes ->
SignalServiceProtos.DataMessage.parseFrom(bytes)
}
} }

View file

@ -23,7 +23,6 @@ import org.session.libsession.snode.SnodeMessage
import org.session.libsession.utilities.SSKEnvironment import org.session.libsession.utilities.SSKEnvironment
import org.session.libsignal.service.internal.push.PushTransportDetails import org.session.libsignal.service.internal.push.PushTransportDetails
import org.session.libsignal.service.internal.push.SignalServiceProtos import org.session.libsignal.service.internal.push.SignalServiceProtos
import org.session.libsignal.service.loki.api.crypto.ProofOfWork
import org.session.libsignal.service.loki.utilities.hexEncodedPublicKey import org.session.libsignal.service.loki.utilities.hexEncodedPublicKey
import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.Base64
import org.session.libsignal.utilities.logging.Log import org.session.libsignal.utilities.logging.Log
@ -153,9 +152,8 @@ object MessageSender {
} }
val recipient = message.recipient!! val recipient = message.recipient!!
val base64EncodedData = Base64.encodeBytes(wrappedMessage) val base64EncodedData = Base64.encodeBytes(wrappedMessage)
val nonce = ProofOfWork.calculate(base64EncodedData, recipient, message.sentTimestamp!!, message.ttl.toInt()) ?: throw Error.ProofOfWorkCalculationFailed
// Send the result // Send the result
val snodeMessage = SnodeMessage(recipient, base64EncodedData, message.ttl, message.sentTimestamp!!, nonce) val snodeMessage = SnodeMessage(recipient, base64EncodedData, message.ttl, message.sentTimestamp!!)
if (destination is Destination.Contact && message is VisibleMessage && !isSelfSend) { if (destination is Destination.Contact && message is VisibleMessage && !isSelfSend) {
SnodeConfiguration.shared.broadcaster.broadcast("sendingMessage", message.sentTimestamp!!) SnodeConfiguration.shared.broadcaster.broadcast("sendingMessage", message.sentTimestamp!!)
} }

View file

@ -1,12 +1,12 @@
package org.session.libsession.messaging.sending_receiving.pollers package org.session.libsession.messaging.sending_receiving.pollers
import com.google.protobuf.ByteString
import nl.komponents.kovenant.Promise import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.deferred import nl.komponents.kovenant.deferred
import org.session.libsession.messaging.MessagingConfiguration import org.session.libsession.messaging.MessagingConfiguration
import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.jobs.JobQueue
import org.session.libsession.messaging.jobs.MessageReceiveJob import org.session.libsession.messaging.jobs.MessageReceiveJob
import org.session.libsession.messaging.opengroups.* import org.session.libsession.messaging.opengroups.OpenGroupAPIV2
import org.session.libsession.messaging.opengroups.OpenGroupV2
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
import org.session.libsignal.utilities.successBackground import org.session.libsignal.utilities.successBackground
@ -22,17 +22,11 @@ class OpenGroupV2Poller(private val openGroup: OpenGroupV2, private val executor
private val cancellableFutures = mutableListOf<ScheduledFuture<out Any>>() private val cancellableFutures = mutableListOf<ScheduledFuture<out Any>>()
// region Convenience
private val userHexEncodedPublicKey = MessagingConfiguration.shared.storage.getUserPublicKey() ?: ""
private var displayNameUpdates = setOf<String>()
// endregion
// region Settings // region Settings
companion object { companion object {
private val pollForNewMessagesInterval: Long = 10 * 1000 private val pollForNewMessagesInterval: Long = 10 * 1000
private val pollForDeletedMessagesInterval: Long = 60 * 1000 private val pollForDeletedMessagesInterval: Long = 60 * 1000
private val pollForModeratorsInterval: Long = 10 * 60 * 1000 private val pollForModeratorsInterval: Long = 10 * 60 * 1000
private val pollForDisplayNamesInterval: Long = 60 * 1000
} }
// endregion // endregion
@ -43,7 +37,6 @@ class OpenGroupV2Poller(private val openGroup: OpenGroupV2, private val executor
executorService.scheduleAtFixedRate(::pollForNewMessages,0, pollForNewMessagesInterval, TimeUnit.MILLISECONDS), executorService.scheduleAtFixedRate(::pollForNewMessages,0, pollForNewMessagesInterval, TimeUnit.MILLISECONDS),
executorService.scheduleAtFixedRate(::pollForDeletedMessages,0, pollForDeletedMessagesInterval, TimeUnit.MILLISECONDS), executorService.scheduleAtFixedRate(::pollForDeletedMessages,0, pollForDeletedMessagesInterval, TimeUnit.MILLISECONDS),
executorService.scheduleAtFixedRate(::pollForModerators,0, pollForModeratorsInterval, TimeUnit.MILLISECONDS), executorService.scheduleAtFixedRate(::pollForModerators,0, pollForModeratorsInterval, TimeUnit.MILLISECONDS),
executorService.scheduleAtFixedRate(::pollForDisplayNames,0, pollForDisplayNamesInterval, TimeUnit.MILLISECONDS)
) )
hasStarted = true hasStarted = true
} }
@ -72,103 +65,21 @@ class OpenGroupV2Poller(private val openGroup: OpenGroupV2, private val executor
Log.d("Loki", "received ${messages.size} messages") Log.d("Loki", "received ${messages.size} messages")
messages.forEach { message -> messages.forEach { message ->
try { try {
val senderPublicKey = message.senderPublicKey val senderPublicKey = message.sender!!
fun generateDisplayName(rawDisplayName: String): String {
return "$rawDisplayName (...${senderPublicKey.takeLast(8)})"
}
val senderDisplayName = MessagingConfiguration.shared.storage.getOpenGroupDisplayName(senderPublicKey, openGroup.room, openGroup.server) ?: generateDisplayName(message.displayName)
val id = openGroup.id.toByteArray()
// Main message // Main message
val dataMessageProto = SignalServiceProtos.DataMessage.newBuilder() val dataMessageProto = message.toProto()
val body = if (message.body == message.timestamp.toString()) { "" } else { message.body }
dataMessageProto.setBody(body)
dataMessageProto.setTimestamp(message.timestamp)
// Attachments
val attachmentProtos = message.attachments.mapNotNull { attachment ->
try {
if (attachment.kind != OpenGroupMessage.Attachment.Kind.Attachment) { return@mapNotNull null }
val attachmentProto = SignalServiceProtos.AttachmentPointer.newBuilder()
attachmentProto.setId(attachment.serverID)
attachmentProto.setContentType(attachment.contentType)
attachmentProto.setSize(attachment.size)
attachmentProto.setFileName(attachment.fileName)
attachmentProto.setFlags(attachment.flags)
attachmentProto.setWidth(attachment.width)
attachmentProto.setHeight(attachment.height)
attachment.caption?.let { attachmentProto.setCaption(it) }
attachmentProto.setUrl(attachment.url)
attachmentProto.build()
} catch (e: Exception) {
Log.e("Loki","Failed to parse attachment as proto",e)
null
}
}
dataMessageProto.addAllAttachments(attachmentProtos)
// Link preview
val linkPreview = message.attachments.firstOrNull { it.kind == OpenGroupMessage.Attachment.Kind.LinkPreview }
if (linkPreview != null) {
val linkPreviewProto = SignalServiceProtos.DataMessage.Preview.newBuilder()
linkPreviewProto.setUrl(linkPreview.linkPreviewURL!!)
linkPreviewProto.setTitle(linkPreview.linkPreviewTitle!!)
val attachmentProto = SignalServiceProtos.AttachmentPointer.newBuilder()
attachmentProto.setId(linkPreview.serverID)
attachmentProto.setContentType(linkPreview.contentType)
attachmentProto.setSize(linkPreview.size)
attachmentProto.setFileName(linkPreview.fileName)
attachmentProto.setFlags(linkPreview.flags)
attachmentProto.setWidth(linkPreview.width)
attachmentProto.setHeight(linkPreview.height)
linkPreview.caption?.let { attachmentProto.setCaption(it) }
attachmentProto.setUrl(linkPreview.url)
linkPreviewProto.setImage(attachmentProto.build())
dataMessageProto.addPreview(linkPreviewProto.build())
}
// Quote
val quote = message.quote
if (quote != null) {
val quoteProto = SignalServiceProtos.DataMessage.Quote.newBuilder()
quoteProto.setId(quote.quotedMessageTimestamp)
quoteProto.setAuthor(quote.quoteePublicKey)
if (quote.quotedMessageBody != quote.quotedMessageTimestamp.toString()) { quoteProto.setText(quote.quotedMessageBody) }
dataMessageProto.setQuote(quoteProto.build())
}
val messageServerID = message.serverID
// Profile
val profileProto = SignalServiceProtos.DataMessage.LokiProfile.newBuilder()
profileProto.setDisplayName(senderDisplayName)
val profilePicture = message.profilePicture
if (profilePicture != null) {
profileProto.setProfilePicture(profilePicture.url)
dataMessageProto.setProfileKey(ByteString.copyFrom(profilePicture.profileKey))
}
dataMessageProto.setProfile(profileProto.build())
/* TODO: the signal service proto needs to be synced with iOS
// Open group info
if (messageServerID != null) {
val openGroupProto = PublicChatInfo.newBuilder()
openGroupProto.setServerID(messageServerID)
dataMessageProto.setPublicChatInfo(openGroupProto.build())
}
*/
// Signal group context
val groupProto = SignalServiceProtos.GroupContext.newBuilder()
groupProto.setId(ByteString.copyFrom(id))
groupProto.setType(SignalServiceProtos.GroupContext.Type.DELIVER)
groupProto.setName(openGroup.displayName)
dataMessageProto.setGroup(groupProto.build())
// Content // Content
val content = SignalServiceProtos.Content.newBuilder() val content = SignalServiceProtos.Content.newBuilder()
content.setDataMessage(dataMessageProto.build()) content.dataMessage = dataMessageProto
// Envelope // Envelope
val builder = SignalServiceProtos.Envelope.newBuilder() val builder = SignalServiceProtos.Envelope.newBuilder()
builder.type = SignalServiceProtos.Envelope.Type.UNIDENTIFIED_SENDER builder.type = SignalServiceProtos.Envelope.Type.UNIDENTIFIED_SENDER
builder.source = senderPublicKey builder.source = senderPublicKey
builder.sourceDevice = 1 builder.sourceDevice = 1
builder.setContent(content.build().toByteString()) builder.content = content.build().toByteString()
builder.timestamp = message.timestamp builder.timestamp = message.sentTimestamp
builder.serverTimestamp = message.serverTimestamp
val envelope = builder.build() val envelope = builder.build()
val job = MessageReceiveJob(envelope.toByteArray(), isBackgroundPoll, messageServerID, openGroup.id) val job = MessageReceiveJob(envelope.toByteArray(), isBackgroundPoll, message.serverID, openGroup.id)
Log.d("Loki", "Scheduling Job $job") Log.d("Loki", "Scheduling Job $job")
if (isBackgroundPoll) { if (isBackgroundPoll) {
job.executeAsync().always { deferred.resolve(Unit) } job.executeAsync().always { deferred.resolve(Unit) }
@ -180,46 +91,29 @@ class OpenGroupV2Poller(private val openGroup: OpenGroupV2, private val executor
Log.e("Loki", "Exception parsing message", e) Log.e("Loki", "Exception parsing message", e)
} }
} }
displayNameUpdates = displayNameUpdates + messages.map { it.senderPublicKey }.toSet() - userHexEncodedPublicKey
executorService?.schedule(::pollForDisplayNames, 0, TimeUnit.MILLISECONDS)
isCaughtUp = true isCaughtUp = true
isPollOngoing = false isPollOngoing = false
deferred.resolve(Unit) deferred.resolve(Unit)
}.fail { }.fail {
Log.d("Loki", "Failed to get messages for group chat with ID: ${openGroup.channel} on server: ${openGroup.server}.") Log.e("Loki", "Failed to get messages for group chat with room: ${openGroup.room} on server: ${openGroup.server}.", it)
isPollOngoing = false isPollOngoing = false
} }
return deferred.promise return deferred.promise
} }
private fun pollForDisplayNames() {
if (displayNameUpdates.isEmpty()) { return }
val hexEncodedPublicKeys = displayNameUpdates
displayNameUpdates = setOf()
OpenGroupAPI.getDisplayNames(hexEncodedPublicKeys, openGroup.server).successBackground { mapping ->
for (pair in mapping.entries) {
if (pair.key == userHexEncodedPublicKey) continue
val senderDisplayName = "${pair.value} (...${pair.key.substring(pair.key.count() - 8)})"
MessagingConfiguration.shared.storage.setOpenGroupDisplayName(pair.key, openGroup.channel, openGroup.server, senderDisplayName)
}
}.fail {
displayNameUpdates = displayNameUpdates.union(hexEncodedPublicKeys)
}
}
private fun pollForDeletedMessages() { private fun pollForDeletedMessages() {
OpenGroupAPI.getDeletedMessageServerIDs(openGroup.channel, openGroup.server).success { deletedMessageServerIDs -> OpenGroupAPIV2.getDeletedMessages(openGroup.room, openGroup.server).success { deletedMessageServerIDs ->
val deletedMessageIDs = deletedMessageServerIDs.mapNotNull { MessagingConfiguration.shared.messageDataProvider.getMessageID(it) } val deletedMessageIDs = deletedMessageServerIDs.mapNotNull { MessagingConfiguration.shared.messageDataProvider.getMessageID(it) }
deletedMessageIDs.forEach { deletedMessageIDs.forEach {
MessagingConfiguration.shared.messageDataProvider.deleteMessage(it) MessagingConfiguration.shared.messageDataProvider.deleteMessage(it)
} }
}.fail { }.fail {
Log.d("Loki", "Failed to get deleted messages for group chat with ID: ${openGroup.channel} on server: ${openGroup.server}.") Log.d("Loki", "Failed to get deleted messages for group chat with ID: ${openGroup.room} on server: ${openGroup.server}.")
} }
} }
private fun pollForModerators() { private fun pollForModerators() {
OpenGroupAPI.getModerators(openGroup.channel, openGroup.server) OpenGroupAPIV2.getModerators(openGroup.room, openGroup.server)
} }
// endregion // endregion
} }

View file

@ -7,17 +7,15 @@ import nl.komponents.kovenant.functional.bind
import nl.komponents.kovenant.functional.map import nl.komponents.kovenant.functional.map
import okhttp3.Request import okhttp3.Request
import org.session.libsession.utilities.AESGCM import org.session.libsession.utilities.AESGCM
import org.session.libsignal.utilities.logging.Log import org.session.libsession.utilities.AESGCM.EncryptionResult
import org.session.libsignal.utilities.Base64 import org.session.libsession.utilities.getBodyForOnionRequest
import org.session.libsignal.utilities.* import org.session.libsession.utilities.getHeadersForOnionRequest
import org.session.libsignal.service.loki.api.Snode import org.session.libsignal.service.loki.api.Snode
import org.session.libsignal.service.loki.api.fileserver.FileServerAPI import org.session.libsignal.service.loki.api.fileserver.FileServerAPI
import org.session.libsignal.service.loki.api.utilities.* import org.session.libsignal.service.loki.api.utilities.*
import org.session.libsession.utilities.AESGCM.EncryptionResult
import org.session.libsignal.utilities.ThreadUtils
import org.session.libsession.utilities.getBodyForOnionRequest
import org.session.libsession.utilities.getHeadersForOnionRequest
import org.session.libsignal.service.loki.utilities.* import org.session.libsignal.service.loki.utilities.*
import org.session.libsignal.utilities.*
import org.session.libsignal.utilities.logging.Log
private typealias Path = List<Snode> private typealias Path = List<Snode>
@ -323,7 +321,7 @@ object OnionRequestAPI {
val plaintext = AESGCM.decrypt(ivAndCiphertext, destinationSymmetricKey) val plaintext = AESGCM.decrypt(ivAndCiphertext, destinationSymmetricKey)
try { try {
@Suppress("NAME_SHADOWING") val json = JsonUtil.fromJson(plaintext.toString(Charsets.UTF_8), Map::class.java) @Suppress("NAME_SHADOWING") val json = JsonUtil.fromJson(plaintext.toString(Charsets.UTF_8), Map::class.java)
val statusCode = json["status"] as Int val statusCode = json["status_code"] as? Int ?: json["status"] as Int
if (statusCode == 406) { if (statusCode == 406) {
@Suppress("NAME_SHADOWING") val body = mapOf( "result" to "Your clock is out of sync with the service node network." ) @Suppress("NAME_SHADOWING") val body = mapOf( "result" to "Your clock is out of sync with the service node network." )
val exception = HTTPRequestFailedAtDestinationException(statusCode, body) val exception = HTTPRequestFailedAtDestinationException(statusCode, body)

View file

@ -12,6 +12,7 @@ import org.session.libsignal.service.loki.api.utilities.HTTP
import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol
import org.session.libsignal.service.loki.utilities.Broadcaster import org.session.libsignal.service.loki.utilities.Broadcaster
import org.session.libsignal.service.loki.utilities.prettifiedDescription import org.session.libsignal.service.loki.utilities.prettifiedDescription
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
import org.session.libsignal.service.loki.utilities.retryIfNeeded import org.session.libsignal.service.loki.utilities.retryIfNeeded
import org.session.libsignal.utilities.* import org.session.libsignal.utilities.*
import org.session.libsignal.utilities.logging.Log import org.session.libsignal.utilities.logging.Log
@ -37,16 +38,18 @@ object SnodeAPI {
// use port 4433 if API level can handle network security config and enforce pinned certificates // use port 4433 if API level can handle network security config and enforce pinned certificates
private val seedPort = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) 443 else 4433 private val seedPort = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) 443 else 4433
private val seedNodePool: Set<String> = setOf( private val seedNodePool by lazy {
"https://storage.seed1.loki.network:$seedPort", if (useTestnet) {
"https://storage.seed3.loki.network:$seedPort", setOf( "http://public.loki.foundation:38157" )
"https://public.loki.foundation:$seedPort" } else {
) setOf( "https://storage.seed1.loki.network:$seedPort", "https://storage.seed3.loki.network:$seedPort", "https://public.loki.foundation:$seedPort" )
internal val snodeFailureThreshold = 4 }
}
private val snodeFailureThreshold = 4
private val targetSwarmSnodeCount = 2 private val targetSwarmSnodeCount = 2
private val useOnionRequests = true private val useOnionRequests = true
internal val useTestnet = true
internal var powDifficulty = 1 internal var powDifficulty = 1
// Error // Error
@ -164,7 +167,7 @@ object SnodeAPI {
cachedSwarmCopy.addAll(cachedSwarm) cachedSwarmCopy.addAll(cachedSwarm)
return task { cachedSwarmCopy } return task { cachedSwarmCopy }
} else { } else {
val parameters = mapOf( "pubKey" to publicKey ) val parameters = mapOf( "pubKey" to if (useTestnet) publicKey.removing05PrefixIfNeeded() else publicKey )
return getRandomSnode().bind { return getRandomSnode().bind {
invoke(Snode.Method.GetSwarm, it, publicKey, parameters) invoke(Snode.Method.GetSwarm, it, publicKey, parameters)
}.map(sharedContext) { }.map(sharedContext) {
@ -177,7 +180,7 @@ object SnodeAPI {
fun getRawMessages(snode: Snode, publicKey: String): RawResponsePromise { fun getRawMessages(snode: Snode, publicKey: String): RawResponsePromise {
val lastHashValue = database.getLastMessageHashValue(snode, publicKey) ?: "" val lastHashValue = database.getLastMessageHashValue(snode, publicKey) ?: ""
val parameters = mapOf( "pubKey" to publicKey, "lastHash" to lastHashValue ) val parameters = mapOf( "pubKey" to if (useTestnet) publicKey.removing05PrefixIfNeeded() else publicKey, "lastHash" to lastHashValue )
return invoke(Snode.Method.GetMessages, snode, publicKey, parameters) return invoke(Snode.Method.GetMessages, snode, publicKey, parameters)
} }
@ -190,7 +193,7 @@ object SnodeAPI {
} }
fun sendMessage(message: SnodeMessage): Promise<Set<RawResponsePromise>, Exception> { fun sendMessage(message: SnodeMessage): Promise<Set<RawResponsePromise>, Exception> {
val destination = message.recipient val destination = if (useTestnet) message.recipient.removing05PrefixIfNeeded() else message.recipient
return retryIfNeeded(maxRetryCount) { return retryIfNeeded(maxRetryCount) {
getTargetSnodes(destination).map { swarm -> getTargetSnodes(destination).map { swarm ->
swarm.map { snode -> swarm.map { snode ->

View file

@ -1,5 +1,7 @@
package org.session.libsession.snode package org.session.libsession.snode
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
data class SnodeMessage( data class SnodeMessage(
// The hex encoded public key of the recipient. // The hex encoded public key of the recipient.
val recipient: String, val recipient: String,
@ -8,16 +10,14 @@ data class SnodeMessage(
// The time to live for the message in milliseconds. // The time to live for the message in milliseconds.
val ttl: Long, val ttl: Long,
// When the proof of work was calculated. // When the proof of work was calculated.
val timestamp: Long, val timestamp: Long
// The base 64 encoded proof of work.
val nonce: String
) { ) {
internal fun toJSON(): Map<String, String> { internal fun toJSON(): Map<String, String> {
return mutableMapOf( return mutableMapOf(
"pubKey" to recipient, "pubKey" to if (SnodeAPI.useTestnet) recipient.removing05PrefixIfNeeded() else recipient,
"data" to data, "data" to data,
"ttl" to ttl.toString(), "ttl" to ttl.toString(),
"timestamp" to timestamp.toString(), "timestamp" to timestamp.toString(),
"nonce" to nonce) "nonce" to "")
} }
} }

View file

@ -25,13 +25,19 @@ class SwarmAPI private constructor(private val database: LokiAPIDatabaseProtocol
companion object { companion object {
private const val useTestnet = true
// use port 4433 if API level can handle network security config and enforce pinned certificates // use port 4433 if API level can handle network security config and enforce pinned certificates
private val seedPort = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) 443 else 4433 private val seedPort = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) 443 else 4433
private val seedNodePool: Set<String> = setOf( private val seedNodePool: Set<String> = if (useTestnet) {
setOf("http://public.loki.foundation:38157")
} else {
setOf(
"https://storage.seed1.loki.network:$seedPort", "https://storage.seed1.loki.network:$seedPort",
"https://storage.seed3.loki.network:$seedPort", "https://storage.seed3.loki.network:$seedPort",
"https://public.loki.foundation:$seedPort" "https://public.loki.foundation:$seedPort"
) )
}
// region Settings // region Settings
private val minimumSnodePoolCount = 64 private val minimumSnodePoolCount = 64