mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-23 18:15:22 +00:00
feat: adding default group handling to frontend viewmodel
This commit is contained in:
parent
aea23a6fc1
commit
1e164f8648
@ -9,8 +9,7 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentPagerAdapter
|
||||
import androidx.fragment.app.*
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.android.synthetic.main.activity_join_public_chat.*
|
||||
import kotlinx.android.synthetic.main.fragment_enter_chat_url.*
|
||||
@ -18,13 +17,16 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import network.loki.messenger.R
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||
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.ScanQRCodeWrapperFragmentDelegate
|
||||
import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol
|
||||
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 {
|
||||
private val adapter = JoinPublicChatActivityAdapter(this)
|
||||
@ -122,14 +124,34 @@ private class JoinPublicChatActivityAdapter(val activity: JoinPublicChatActivity
|
||||
// region Enter Chat URL 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 {
|
||||
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?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
chatURLEditText.imeOptions = chatURLEditText.imeOptions or 16777216 // Always use incognito keyboard
|
||||
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() {
|
||||
|
@ -30,7 +30,7 @@ class PublicChatManager(private val context: Context) {
|
||||
public fun areAllCaughtUp(): Boolean {
|
||||
var areAllCaughtUp = true
|
||||
refreshChatsAndPollers()
|
||||
for ((threadID, chat) in chats) {
|
||||
for ((threadID, _) in chats) {
|
||||
val poller = pollers[threadID]
|
||||
areAllCaughtUp = if (poller != null) areAllCaughtUp && poller.isCaughtUp else true
|
||||
}
|
||||
@ -83,9 +83,9 @@ class PublicChatManager(private val context: Context) {
|
||||
@WorkerThread
|
||||
fun addChat(server: String, room: String): OpenGroupV2 {
|
||||
// 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)
|
||||
}
|
||||
|
||||
@ -116,17 +116,19 @@ class PublicChatManager(private val context: Context) {
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
fun addChat(server: String, room: String, info: OpenGroupInfo): OpenGroupV2 {
|
||||
val chat = OpenGroupV2(server, room, info.displayName, )
|
||||
var threadID = GroupManager.getOpenGroupThreadID(chat.id, context)
|
||||
fun addChat(server: String, room: String, info: OpenGroupAPIV2.Info): OpenGroupV2 {
|
||||
val chat = OpenGroupV2(server, room, info.id, info.name, info.imageID)
|
||||
val threadID = GroupManager.getOpenGroupThreadID(chat.id, context)
|
||||
var profilePicture: Bitmap? = null
|
||||
if (threadID < 0) {
|
||||
if (info.profilePictureURL.isNotEmpty()) {
|
||||
val profilePictureAsByteArray = OpenGroupAPIV2.downloadOpenGroupProfilePicture(server, info.profilePictureURL)
|
||||
val imageID = info.imageID
|
||||
if (!imageID.isNullOrEmpty()) {
|
||||
val profilePictureAsByteArray = OpenGroupAPIV2.downloadOpenGroupProfilePicture(imageID)
|
||||
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) {
|
||||
|
@ -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?)
|
@ -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>()
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/contentView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
@ -22,12 +22,49 @@
|
||||
android:layout_marginTop="@dimen/large_spacing"
|
||||
android:layout_marginRight="@dimen/large_spacing"
|
||||
android:inputType="textWebEmailAddress"
|
||||
android:hint="Enter an open group URL" />
|
||||
android:hint="@string/fragment_enter_chat_url_edit_text_hint" />
|
||||
|
||||
<View
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
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
|
||||
style="@style/Widget.Session.Button.Common.ProminentOutline"
|
||||
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/contentView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
@ -29,6 +29,14 @@
|
||||
android:layout_height="0dp"
|
||||
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
|
||||
style="@style/Widget.Session.Button.Common.ProminentOutline"
|
||||
android:id="@+id/joinPublicChatButton"
|
||||
|
@ -3,7 +3,7 @@
|
||||
<domain-config cleartextTrafficPermitted="true">
|
||||
<domain includeSubdomains="true">127.0.0.1</domain>
|
||||
</domain-config>
|
||||
<domain-config cleartextTrafficPermitted="false">
|
||||
<domain-config cleartextTrafficPermitted="true">
|
||||
<domain includeSubdomains="false">public.loki.foundation</domain>
|
||||
<trust-anchors>
|
||||
<certificates src="@raw/lf_session_cert"/>
|
||||
@ -21,4 +21,13 @@
|
||||
<certificates src="@raw/seed3cert"/>
|
||||
</trust-anchors>
|
||||
</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>
|
@ -1,5 +1,7 @@
|
||||
package org.session.libsession.messaging.opengroups
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import nl.komponents.kovenant.Kovenant
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.functional.bind
|
||||
@ -8,11 +10,13 @@ import okhttp3.HttpUrl
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.RequestBody
|
||||
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.snode.OnionRequestAPI
|
||||
import org.session.libsession.utilities.AESGCM
|
||||
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.utilities.DownloadUtilities
|
||||
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
|
||||
import org.session.libsignal.service.loki.utilities.toHexString
|
||||
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.logging.Log
|
||||
import org.whispersystems.curve25519.Curve25519
|
||||
import java.io.ByteArrayOutputStream
|
||||
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 = "https://sessionopengroup.com"
|
||||
const val DEFAULT_SERVER_PUBLIC_KEY = "658d29b91892a2389505596b135e76a53db6e11d613a51dbd3d0816adffb231b"
|
||||
private const val DEFAULT_SERVER = "https://sog.ibolpap.finance"
|
||||
private const val DEFAULT_SERVER_PUBLIC_KEY = "b464aa186530c97d6bcf663a3a3b7465a5f782beaa67c83bee99468824b4aa10"
|
||||
|
||||
val defaultRooms = MutableSharedFlow<List<Info>>(replay = 1)
|
||||
|
||||
private val sharedContext = Kovenant.createContext()
|
||||
private val curve = Curve25519.getInstance(Curve25519.BEST)
|
||||
@ -65,7 +72,7 @@ object OpenGroupAPIV2 {
|
||||
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 urlBuilder = HttpUrl.Builder()
|
||||
.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> {
|
||||
val storage = MessagingConfiguration.shared.storage
|
||||
return storage.getAuthToken(room, server)?.let {
|
||||
@ -136,9 +158,11 @@ object OpenGroupAPIV2 {
|
||||
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(sharedContext) { 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.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 ciphertext = decode(base64EncodedCiphertext)
|
||||
val ephemeralPublicKey = decode(base64EncodedEphemeralPublicKey)
|
||||
val symmetricKey = AESGCM.generateSymmetricKey(ephemeralPublicKey, privateKey)
|
||||
@ -189,7 +213,8 @@ object OpenGroupAPIV2 {
|
||||
val json = signedMessage.toJSON()
|
||||
val request = Request(verb = POST, room = room, server = server, endpoint = "messages", parameters = json)
|
||||
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
|
||||
}
|
||||
}
|
||||
@ -198,13 +223,14 @@ object OpenGroupAPIV2 {
|
||||
// region Messages
|
||||
fun getMessages(room: String, server: String): Promise<List<OpenGroupMessageV2>, Exception> {
|
||||
val storage = MessagingConfiguration.shared.storage
|
||||
val queryParameters = mutableMapOf<String,String>()
|
||||
storage.getLastMessageServerId(room,server)?.let { lastId ->
|
||||
val queryParameters = mutableMapOf<String, String>()
|
||||
storage.getLastMessageServerId(room, server)?.let { lastId ->
|
||||
queryParameters += "from_server_id" to lastId.toString()
|
||||
}
|
||||
val request = Request(verb = GET, room = room, server = server, endpoint = "messages", queryParameters = queryParameters)
|
||||
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
|
||||
|
||||
var currentMax = lastMessageServerId
|
||||
@ -225,7 +251,7 @@ object OpenGroupAPIV2 {
|
||||
}
|
||||
message
|
||||
}
|
||||
storage.setLastMessageServerId(room,server,currentMax)
|
||||
storage.setLastMessageServerId(room, server, currentMax)
|
||||
messages
|
||||
}
|
||||
}
|
||||
@ -241,13 +267,14 @@ object OpenGroupAPIV2 {
|
||||
|
||||
fun getDeletedMessages(room: String, server: String): Promise<List<Long>, Exception> {
|
||||
val storage = MessagingConfiguration.shared.storage
|
||||
val queryParameters = mutableMapOf<String,String>()
|
||||
val queryParameters = mutableMapOf<String, String>()
|
||||
storage.getLastDeletionServerId(room, server)?.let { last ->
|
||||
queryParameters["from_server_id"] = last.toString()
|
||||
}
|
||||
val request = Request(verb = GET, room = room, server = server, endpoint = "deleted_messages", queryParameters = queryParameters)
|
||||
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 serverID = serverIDs.maxOrNull() ?: 0
|
||||
if (serverID > lastMessageServerId) {
|
||||
@ -262,7 +289,8 @@ object OpenGroupAPIV2 {
|
||||
fun getModerators(room: String, server: String): Promise<List<String>, Exception> {
|
||||
val request = Request(verb = GET, room = room, server = server, endpoint = "moderators")
|
||||
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"
|
||||
moderators[id] = moderatorsJson.toMutableSet()
|
||||
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
|
||||
|
||||
// region General
|
||||
fun getDefaultRoomsIfNeeded(): Promise<List<Info>, Exception> {
|
||||
val storage = MessagingConfiguration.shared.storage
|
||||
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> {
|
||||
val request = Request(verb = GET, room = room, server = server, endpoint = "rooms/$room", isAuthRequired = false)
|
||||
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 name = rawRoom["name"] as? String ?: throw Error.PARSING_FAILED
|
||||
val imageID = rawRoom["image_id"] as? String
|
||||
@ -308,7 +339,7 @@ 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(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 {
|
||||
val roomJson = it as? Map<*, *> ?: return@mapNotNull null
|
||||
val id = roomJson["id"] as? String ?: return@mapNotNull null
|
||||
|
@ -1,7 +1,9 @@
|
||||
package org.session.libsession.messaging.opengroups
|
||||
|
||||
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.decode
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.whispersystems.curve25519.Curve25519
|
||||
|
||||
@ -58,4 +60,9 @@ data class OpenGroupMessageV2(
|
||||
base64EncodedSignature?.let { jsonMap["signature"] = base64EncodedSignature }
|
||||
return jsonMap
|
||||
}
|
||||
|
||||
fun toProto(): SignalServiceProtos.DataMessage = decode(base64EncodedData).let { bytes ->
|
||||
SignalServiceProtos.DataMessage.parseFrom(bytes)
|
||||
}
|
||||
|
||||
}
|
@ -23,7 +23,6 @@ import org.session.libsession.snode.SnodeMessage
|
||||
import org.session.libsession.utilities.SSKEnvironment
|
||||
import org.session.libsignal.service.internal.push.PushTransportDetails
|
||||
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.utilities.Base64
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
@ -153,9 +152,8 @@ object MessageSender {
|
||||
}
|
||||
val recipient = message.recipient!!
|
||||
val base64EncodedData = Base64.encodeBytes(wrappedMessage)
|
||||
val nonce = ProofOfWork.calculate(base64EncodedData, recipient, message.sentTimestamp!!, message.ttl.toInt()) ?: throw Error.ProofOfWorkCalculationFailed
|
||||
// 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) {
|
||||
SnodeConfiguration.shared.broadcaster.broadcast("sendingMessage", message.sentTimestamp!!)
|
||||
}
|
||||
|
@ -1,12 +1,12 @@
|
||||
package org.session.libsession.messaging.sending_receiving.pollers
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.deferred
|
||||
import org.session.libsession.messaging.MessagingConfiguration
|
||||
import org.session.libsession.messaging.jobs.JobQueue
|
||||
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.utilities.logging.Log
|
||||
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>>()
|
||||
|
||||
// region Convenience
|
||||
private val userHexEncodedPublicKey = MessagingConfiguration.shared.storage.getUserPublicKey() ?: ""
|
||||
private var displayNameUpdates = setOf<String>()
|
||||
// endregion
|
||||
|
||||
// region Settings
|
||||
companion object {
|
||||
private val pollForNewMessagesInterval: Long = 10 * 1000
|
||||
private val pollForDeletedMessagesInterval: Long = 60 * 1000
|
||||
private val pollForModeratorsInterval: Long = 10 * 60 * 1000
|
||||
private val pollForDisplayNamesInterval: Long = 60 * 1000
|
||||
}
|
||||
// endregion
|
||||
|
||||
@ -43,7 +37,6 @@ class OpenGroupV2Poller(private val openGroup: OpenGroupV2, private val executor
|
||||
executorService.scheduleAtFixedRate(::pollForNewMessages,0, pollForNewMessagesInterval, TimeUnit.MILLISECONDS),
|
||||
executorService.scheduleAtFixedRate(::pollForDeletedMessages,0, pollForDeletedMessagesInterval, TimeUnit.MILLISECONDS),
|
||||
executorService.scheduleAtFixedRate(::pollForModerators,0, pollForModeratorsInterval, TimeUnit.MILLISECONDS),
|
||||
executorService.scheduleAtFixedRate(::pollForDisplayNames,0, pollForDisplayNamesInterval, TimeUnit.MILLISECONDS)
|
||||
)
|
||||
hasStarted = true
|
||||
}
|
||||
@ -72,103 +65,21 @@ class OpenGroupV2Poller(private val openGroup: OpenGroupV2, private val executor
|
||||
Log.d("Loki", "received ${messages.size} messages")
|
||||
messages.forEach { message ->
|
||||
try {
|
||||
val senderPublicKey = message.senderPublicKey
|
||||
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()
|
||||
val senderPublicKey = message.sender!!
|
||||
// Main message
|
||||
val dataMessageProto = SignalServiceProtos.DataMessage.newBuilder()
|
||||
val body = if (message.body == message.timestamp.toString()) { "" } else { message.body }
|
||||
dataMessageProto.setBody(body)
|
||||
dataMessageProto.setTimestamp(message.timestamp)
|
||||
// Attachments
|
||||
val attachmentProtos = message.attachments.mapNotNull { attachment ->
|
||||
try {
|
||||
if (attachment.kind != OpenGroupMessage.Attachment.Kind.Attachment) { return@mapNotNull null }
|
||||
val attachmentProto = 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())
|
||||
val dataMessageProto = message.toProto()
|
||||
// Content
|
||||
val content = SignalServiceProtos.Content.newBuilder()
|
||||
content.setDataMessage(dataMessageProto.build())
|
||||
content.dataMessage = dataMessageProto
|
||||
// Envelope
|
||||
val builder = SignalServiceProtos.Envelope.newBuilder()
|
||||
builder.type = SignalServiceProtos.Envelope.Type.UNIDENTIFIED_SENDER
|
||||
builder.source = senderPublicKey
|
||||
builder.sourceDevice = 1
|
||||
builder.setContent(content.build().toByteString())
|
||||
builder.timestamp = message.timestamp
|
||||
builder.serverTimestamp = message.serverTimestamp
|
||||
builder.content = content.build().toByteString()
|
||||
builder.timestamp = message.sentTimestamp
|
||||
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")
|
||||
if (isBackgroundPoll) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
displayNameUpdates = displayNameUpdates + messages.map { it.senderPublicKey }.toSet() - userHexEncodedPublicKey
|
||||
executorService?.schedule(::pollForDisplayNames, 0, TimeUnit.MILLISECONDS)
|
||||
isCaughtUp = true
|
||||
isPollOngoing = false
|
||||
deferred.resolve(Unit)
|
||||
}.fail {
|
||||
Log.d("Loki", "Failed to get messages for group chat with ID: ${openGroup.channel} on server: ${openGroup.server}.")
|
||||
Log.e("Loki", "Failed to get messages for group chat with room: ${openGroup.room} on server: ${openGroup.server}.", it)
|
||||
isPollOngoing = false
|
||||
}
|
||||
return deferred.promise
|
||||
}
|
||||
|
||||
private fun pollForDisplayNames() {
|
||||
if (displayNameUpdates.isEmpty()) { return }
|
||||
val hexEncodedPublicKeys = displayNameUpdates
|
||||
displayNameUpdates = setOf()
|
||||
OpenGroupAPI.getDisplayNames(hexEncodedPublicKeys, openGroup.server).successBackground { mapping ->
|
||||
for (pair in mapping.entries) {
|
||||
if (pair.key == userHexEncodedPublicKey) continue
|
||||
val senderDisplayName = "${pair.value} (...${pair.key.substring(pair.key.count() - 8)})"
|
||||
MessagingConfiguration.shared.storage.setOpenGroupDisplayName(pair.key, openGroup.channel, openGroup.server, senderDisplayName)
|
||||
}
|
||||
}.fail {
|
||||
displayNameUpdates = displayNameUpdates.union(hexEncodedPublicKeys)
|
||||
}
|
||||
}
|
||||
|
||||
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) }
|
||||
deletedMessageIDs.forEach {
|
||||
MessagingConfiguration.shared.messageDataProvider.deleteMessage(it)
|
||||
}
|
||||
}.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() {
|
||||
OpenGroupAPI.getModerators(openGroup.channel, openGroup.server)
|
||||
OpenGroupAPIV2.getModerators(openGroup.room, openGroup.server)
|
||||
}
|
||||
// endregion
|
||||
}
|
@ -7,17 +7,15 @@ import nl.komponents.kovenant.functional.bind
|
||||
import nl.komponents.kovenant.functional.map
|
||||
import okhttp3.Request
|
||||
import org.session.libsession.utilities.AESGCM
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsignal.utilities.Base64
|
||||
import org.session.libsignal.utilities.*
|
||||
import org.session.libsession.utilities.AESGCM.EncryptionResult
|
||||
import org.session.libsession.utilities.getBodyForOnionRequest
|
||||
import org.session.libsession.utilities.getHeadersForOnionRequest
|
||||
import org.session.libsignal.service.loki.api.Snode
|
||||
import org.session.libsignal.service.loki.api.fileserver.FileServerAPI
|
||||
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.utilities.*
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
|
||||
private typealias Path = List<Snode>
|
||||
|
||||
@ -323,7 +321,7 @@ object OnionRequestAPI {
|
||||
val plaintext = AESGCM.decrypt(ivAndCiphertext, destinationSymmetricKey)
|
||||
try {
|
||||
@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) {
|
||||
@Suppress("NAME_SHADOWING") val body = mapOf( "result" to "Your clock is out of sync with the service node network." )
|
||||
val exception = HTTPRequestFailedAtDestinationException(statusCode, body)
|
||||
|
@ -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.utilities.Broadcaster
|
||||
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.utilities.*
|
||||
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
|
||||
private val seedPort = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) 443 else 4433
|
||||
private val seedNodePool: Set<String> = setOf(
|
||||
"https://storage.seed1.loki.network:$seedPort",
|
||||
"https://storage.seed3.loki.network:$seedPort",
|
||||
"https://public.loki.foundation:$seedPort"
|
||||
)
|
||||
internal val snodeFailureThreshold = 4
|
||||
private val seedNodePool by lazy {
|
||||
if (useTestnet) {
|
||||
setOf( "http://public.loki.foundation:38157" )
|
||||
} else {
|
||||
setOf( "https://storage.seed1.loki.network:$seedPort", "https://storage.seed3.loki.network:$seedPort", "https://public.loki.foundation:$seedPort" )
|
||||
}
|
||||
}
|
||||
private val snodeFailureThreshold = 4
|
||||
private val targetSwarmSnodeCount = 2
|
||||
|
||||
private val useOnionRequests = true
|
||||
|
||||
internal val useTestnet = true
|
||||
internal var powDifficulty = 1
|
||||
|
||||
// Error
|
||||
@ -164,7 +167,7 @@ object SnodeAPI {
|
||||
cachedSwarmCopy.addAll(cachedSwarm)
|
||||
return task { cachedSwarmCopy }
|
||||
} else {
|
||||
val parameters = mapOf( "pubKey" to publicKey )
|
||||
val parameters = mapOf( "pubKey" to if (useTestnet) publicKey.removing05PrefixIfNeeded() else publicKey )
|
||||
return getRandomSnode().bind {
|
||||
invoke(Snode.Method.GetSwarm, it, publicKey, parameters)
|
||||
}.map(sharedContext) {
|
||||
@ -177,7 +180,7 @@ object SnodeAPI {
|
||||
|
||||
fun getRawMessages(snode: Snode, publicKey: String): RawResponsePromise {
|
||||
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)
|
||||
}
|
||||
|
||||
@ -190,7 +193,7 @@ object SnodeAPI {
|
||||
}
|
||||
|
||||
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) {
|
||||
getTargetSnodes(destination).map { swarm ->
|
||||
swarm.map { snode ->
|
||||
|
@ -1,5 +1,7 @@
|
||||
package org.session.libsession.snode
|
||||
|
||||
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
|
||||
|
||||
data class SnodeMessage(
|
||||
// The hex encoded public key of the recipient.
|
||||
val recipient: String,
|
||||
@ -8,16 +10,14 @@ data class SnodeMessage(
|
||||
// The time to live for the message in milliseconds.
|
||||
val ttl: Long,
|
||||
// When the proof of work was calculated.
|
||||
val timestamp: Long,
|
||||
// The base 64 encoded proof of work.
|
||||
val nonce: String
|
||||
val timestamp: Long
|
||||
) {
|
||||
internal fun toJSON(): Map<String, String> {
|
||||
return mutableMapOf(
|
||||
"pubKey" to recipient,
|
||||
"pubKey" to if (SnodeAPI.useTestnet) recipient.removing05PrefixIfNeeded() else recipient,
|
||||
"data" to data,
|
||||
"ttl" to ttl.toString(),
|
||||
"timestamp" to timestamp.toString(),
|
||||
"nonce" to nonce)
|
||||
"nonce" to "")
|
||||
}
|
||||
}
|
||||
|
@ -25,13 +25,19 @@ class SwarmAPI private constructor(private val database: LokiAPIDatabaseProtocol
|
||||
|
||||
companion object {
|
||||
|
||||
private const val useTestnet = true
|
||||
|
||||
// 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 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.seed3.loki.network:$seedPort",
|
||||
"https://public.loki.foundation:$seedPort"
|
||||
)
|
||||
}
|
||||
|
||||
// region Settings
|
||||
private val minimumSnodePoolCount = 64
|
||||
|
Loading…
Reference in New Issue
Block a user