feat: adding default group handling to frontend viewmodel

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

View File

@ -9,8 +9,7 @@ import android.view.View
import android.view.ViewGroup
import android.view.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() {

View File

@ -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) {

View File

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

View File

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

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<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"

View File

@ -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"

View File

@ -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>

View File

@ -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

View File

@ -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)
}
}

View File

@ -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!!)
}

View File

@ -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
}

View File

@ -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)

View File

@ -12,6 +12,7 @@ import org.session.libsignal.service.loki.api.utilities.HTTP
import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol
import org.session.libsignal.service.loki.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 ->

View File

@ -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 "")
}
}

View File

@ -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(
"https://storage.seed1.loki.network:$seedPort",
"https://storage.seed3.loki.network:$seedPort",
"https://public.loki.foundation:$seedPort"
)
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