mirror of
https://github.com/oxen-io/session-android.git
synced 2025-02-17 12:18:25 +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.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() {
|
||||||
|
@ -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) {
|
||||||
|
@ -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"?>
|
<?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"
|
||||||
|
@ -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"
|
||||||
|
@ -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>
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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!!)
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
@ -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)
|
||||||
|
@ -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 ->
|
||||||
|
@ -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 "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
"https://storage.seed1.loki.network:$seedPort",
|
setOf("http://public.loki.foundation:38157")
|
||||||
"https://storage.seed3.loki.network:$seedPort",
|
} else {
|
||||||
"https://public.loki.foundation:$seedPort"
|
setOf(
|
||||||
)
|
"https://storage.seed1.loki.network:$seedPort",
|
||||||
|
"https://storage.seed3.loki.network:$seedPort",
|
||||||
|
"https://public.loki.foundation:$seedPort"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// region Settings
|
// region Settings
|
||||||
private val minimumSnodePoolCount = 64
|
private val minimumSnodePoolCount = 64
|
||||||
|
Loading…
x
Reference in New Issue
Block a user