mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-27 12:05:22 +00:00
Tweaked some open group handling and a couple of onboarding issues
Updated the OpenGroup adding and polling logic to reduce duplicate API calls Updated the BackgroundGroupAddJob to start a GroupAvatarDownloadJob instead of running the download itself (to appear to run faster) Defaulted OpenGroups to use blinded auth when no server capabilities are present Fixed an issue where the background poller could be started even though the onboarding hadn't been completed Fixed an issue where the database could get into an invalid state if the app was restarted during onboarding
This commit is contained in:
parent
d0a4bac83e
commit
5afd647686
@ -245,6 +245,12 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
||||
Log.i(TAG, "App is now visible.");
|
||||
KeyCachingService.onAppForegrounded(this);
|
||||
|
||||
// If the user account hasn't been created or onboarding wasn't finished then don't start
|
||||
// the pollers
|
||||
if (TextSecurePreferences.getLocalNumber(this) == null || !TextSecurePreferences.hasSeenWelcomeScreen(this)) {
|
||||
return;
|
||||
}
|
||||
|
||||
ThreadUtils.queue(()->{
|
||||
if (poller != null) {
|
||||
poller.setCaughtUp(false);
|
||||
|
@ -300,6 +300,11 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
|
||||
val lastHash = database.insertOrUpdate(lastMessageHashValueTable2, row, query, arrayOf( snode.toString(), publicKey, namespace.toString() ))
|
||||
}
|
||||
|
||||
override fun clearAllLastMessageHashes() {
|
||||
val database = databaseHelper.writableDatabase
|
||||
database.delete(lastMessageHashValueTable2, null, null)
|
||||
}
|
||||
|
||||
override fun getReceivedMessageHashValues(publicKey: String, namespace: Int): Set<String>? {
|
||||
val database = databaseHelper.readableDatabase
|
||||
val query = "${Companion.publicKey} = ? AND ${Companion.receivedMessageHashNamespace} = ?"
|
||||
@ -321,6 +326,11 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
|
||||
database.insertOrUpdate(receivedMessageHashValuesTable, row, query, arrayOf( publicKey, namespace.toString() ))
|
||||
}
|
||||
|
||||
override fun clearReceivedMessageHashValues() {
|
||||
val database = databaseHelper.writableDatabase
|
||||
database.delete(receivedMessageHashValuesTable, null, null)
|
||||
}
|
||||
|
||||
override fun getAuthToken(server: String): String? {
|
||||
val database = databaseHelper.readableDatabase
|
||||
return database.get(openGroupAuthTokenTable, "${Companion.server} = ?", wrap(server)) { cursor ->
|
||||
|
@ -58,14 +58,14 @@ object OpenGroupManager {
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
fun add(server: String, room: String, publicKey: String, context: Context) {
|
||||
fun add(server: String, room: String, publicKey: String, context: Context): OpenGroupApi.RoomInfo? {
|
||||
val openGroupID = "$server.$room"
|
||||
var threadID = GroupManager.getOpenGroupThreadID(openGroupID, context)
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val threadDB = DatabaseComponent.get(context).lokiThreadDatabase()
|
||||
// Check it it's added already
|
||||
val existingOpenGroup = threadDB.getOpenGroupChat(threadID)
|
||||
if (existingOpenGroup != null) { return }
|
||||
if (existingOpenGroup != null) { return null }
|
||||
// Clear any existing data if needed
|
||||
storage.removeLastDeletionServerID(room, server)
|
||||
storage.removeLastMessageServerID(room, server)
|
||||
@ -73,18 +73,17 @@ object OpenGroupManager {
|
||||
storage.removeLastOutboxMessageId(server)
|
||||
// Store the public key
|
||||
storage.setOpenGroupPublicKey(server, publicKey)
|
||||
// Get capabilities
|
||||
val capabilities = OpenGroupApi.getCapabilities(server).get()
|
||||
// Get capabilities & room info
|
||||
val (capabilities, info) = OpenGroupApi.getCapabilitiesAndRoomInfo(room, server).get()
|
||||
storage.setServerCapabilities(server, capabilities.capabilities)
|
||||
// Get room info
|
||||
val info = OpenGroupApi.getRoomInfo(room, server).get()
|
||||
storage.setUserCount(room, server, info.activeUsers)
|
||||
// Create the group locally if not available already
|
||||
if (threadID < 0) {
|
||||
threadID = GroupManager.createOpenGroup(openGroupID, context, null, info.name).threadId
|
||||
}
|
||||
val openGroup = OpenGroup(server, room, info.name, info.infoUpdates, publicKey)
|
||||
val openGroup = OpenGroup(server, room, publicKey, info.name, info.imageId, info.infoUpdates)
|
||||
threadDB.setOpenGroupChat(openGroup, threadID)
|
||||
return info
|
||||
}
|
||||
|
||||
fun restartPollerForServer(server: String) {
|
||||
@ -130,12 +129,13 @@ object OpenGroupManager {
|
||||
}
|
||||
}
|
||||
|
||||
fun addOpenGroup(urlAsString: String, context: Context) {
|
||||
val url = HttpUrl.parse(urlAsString) ?: return
|
||||
fun addOpenGroup(urlAsString: String, context: Context): OpenGroupApi.RoomInfo? {
|
||||
val url = HttpUrl.parse(urlAsString) ?: return null
|
||||
val server = OpenGroup.getServer(urlAsString)
|
||||
val room = url.pathSegments().firstOrNull() ?: return
|
||||
val publicKey = url.queryParameter("public_key") ?: return
|
||||
add(server.toString().removeSuffix("/"), room, publicKey, context) // assume migrated from calling function
|
||||
val room = url.pathSegments().firstOrNull() ?: return null
|
||||
val publicKey = url.queryParameter("public_key") ?: return null
|
||||
|
||||
return add(server.toString().removeSuffix("/"), room, publicKey, context) // assume migrated from calling function
|
||||
}
|
||||
|
||||
fun updateOpenGroup(openGroup: OpenGroup, context: Context) {
|
||||
|
@ -44,7 +44,7 @@ class BackgroundPollWorker(val context: Context, params: WorkerParameters) : Wor
|
||||
}
|
||||
|
||||
override fun doWork(): Result {
|
||||
if (TextSecurePreferences.getLocalNumber(context) == null) {
|
||||
if (TextSecurePreferences.getLocalNumber(context) == null || !TextSecurePreferences.hasSeenWelcomeScreen(context)) {
|
||||
Log.v(TAG, "User not registered yet.")
|
||||
return Result.failure()
|
||||
}
|
||||
|
@ -22,8 +22,10 @@ import kotlinx.coroutines.launch
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ActivityLinkDeviceBinding
|
||||
import network.loki.messenger.databinding.FragmentRecoveryPhraseBinding
|
||||
import org.session.libsession.snode.SnodeModule
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.crypto.MnemonicCodec
|
||||
import org.session.libsignal.database.LokiAPIDatabaseProtocol
|
||||
import org.session.libsignal.utilities.Hex
|
||||
import org.session.libsignal.utilities.KeyHelper
|
||||
import org.session.libsignal.utilities.Log
|
||||
@ -39,6 +41,8 @@ import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo
|
||||
|
||||
class LinkDeviceActivity : BaseActionBarActivity(), ScanQRCodeWrapperFragmentDelegate {
|
||||
private lateinit var binding: ActivityLinkDeviceBinding
|
||||
internal val database: LokiAPIDatabaseProtocol
|
||||
get() = SnodeModule.shared.storage
|
||||
private val adapter = LinkDeviceActivityAdapter(this)
|
||||
private var restoreJob: Job? = null
|
||||
|
||||
@ -99,6 +103,11 @@ class LinkDeviceActivity : BaseActionBarActivity(), ScanQRCodeWrapperFragmentDel
|
||||
if (restoreJob?.isActive == true) return
|
||||
|
||||
restoreJob = lifecycleScope.launch {
|
||||
// This is here to resolve a case where the app restarts before a user completes onboarding
|
||||
// which can result in an invalid database state
|
||||
database.clearAllLastMessageHashes()
|
||||
database.clearReceivedMessageHashValues()
|
||||
|
||||
// RestoreActivity handles seed this way
|
||||
val keyPairGenerationResult = KeyPairUtilities.generate(seed)
|
||||
val x25519KeyPair = keyPairGenerationResult.x25519KeyPair
|
||||
|
@ -13,8 +13,10 @@ import android.view.View
|
||||
import android.widget.Toast
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ActivityRecoveryPhraseRestoreBinding
|
||||
import org.session.libsession.snode.SnodeModule
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.crypto.MnemonicCodec
|
||||
import org.session.libsignal.database.LokiAPIDatabaseProtocol
|
||||
import org.session.libsignal.utilities.Hex
|
||||
import org.session.libsignal.utilities.KeyHelper
|
||||
import org.session.libsignal.utilities.hexEncodedPublicKey
|
||||
@ -26,6 +28,8 @@ import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo
|
||||
|
||||
class RecoveryPhraseRestoreActivity : BaseActionBarActivity() {
|
||||
private lateinit var binding: ActivityRecoveryPhraseRestoreBinding
|
||||
internal val database: LokiAPIDatabaseProtocol
|
||||
get() = SnodeModule.shared.storage
|
||||
// region Lifecycle
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@ -64,6 +68,11 @@ class RecoveryPhraseRestoreActivity : BaseActionBarActivity() {
|
||||
private fun restore() {
|
||||
val mnemonic = binding.mnemonicEditText.text.toString()
|
||||
try {
|
||||
// This is here to resolve a case where the app restarts before a user completes onboarding
|
||||
// which can result in an invalid database state
|
||||
database.clearAllLastMessageHashes()
|
||||
database.clearReceivedMessageHashValues()
|
||||
|
||||
val loadFileContents: (String) -> String = { fileName ->
|
||||
MnemonicUtilities.loadFileContents(this, fileName)
|
||||
}
|
||||
|
@ -18,8 +18,10 @@ import android.widget.Toast
|
||||
import com.goterl.lazysodium.utils.KeyPair
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ActivityRegisterBinding
|
||||
import org.session.libsession.snode.SnodeModule
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.crypto.ecc.ECKeyPair
|
||||
import org.session.libsignal.database.LokiAPIDatabaseProtocol
|
||||
import org.session.libsignal.utilities.KeyHelper
|
||||
import org.session.libsignal.utilities.hexEncodedPublicKey
|
||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||
@ -29,6 +31,8 @@ import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo
|
||||
|
||||
class RegisterActivity : BaseActionBarActivity() {
|
||||
private lateinit var binding: ActivityRegisterBinding
|
||||
internal val database: LokiAPIDatabaseProtocol
|
||||
get() = SnodeModule.shared.storage
|
||||
private var seed: ByteArray? = null
|
||||
private var ed25519KeyPair: KeyPair? = null
|
||||
private var x25519KeyPair: ECKeyPair? = null
|
||||
@ -109,6 +113,11 @@ class RegisterActivity : BaseActionBarActivity() {
|
||||
|
||||
// region Interaction
|
||||
private fun register() {
|
||||
// This is here to resolve a case where the app restarts before a user completes onboarding
|
||||
// which can result in an invalid database state
|
||||
database.clearAllLastMessageHashes()
|
||||
database.clearReceivedMessageHashValues()
|
||||
|
||||
KeyPairUtilities.store(this, seed!!, ed25519KeyPair!!, x25519KeyPair!!)
|
||||
val userHexEncodedPublicKey = x25519KeyPair!!.hexEncodedPublicKey
|
||||
val registrationID = KeyHelper.generateRegistrationId(false)
|
||||
|
@ -41,15 +41,10 @@ class BackgroundGroupAddJob(val joinUrl: String): Job {
|
||||
}
|
||||
// get image
|
||||
storage.setOpenGroupPublicKey(openGroup.server, openGroup.serverPublicKey)
|
||||
val (capabilities, info) = OpenGroupApi.getCapabilitiesAndRoomInfo(openGroup.room, openGroup.server, false).get()
|
||||
storage.setServerCapabilities(openGroup.server, capabilities.capabilities)
|
||||
val imageId = info.imageId
|
||||
storage.addOpenGroup(openGroup.joinUrl())
|
||||
val info = storage.addOpenGroup(openGroup.joinUrl())
|
||||
val imageId = info?.imageId
|
||||
if (imageId != null) {
|
||||
val bytes = OpenGroupApi.downloadOpenGroupProfilePicture(openGroup.server, openGroup.room, imageId).get()
|
||||
val groupId = GroupUtil.getEncodedOpenGroupID("${openGroup.server}.${openGroup.room}".toByteArray())
|
||||
storage.updateProfilePicture(groupId, bytes)
|
||||
storage.updateTimestampUpdated(groupId, System.currentTimeMillis())
|
||||
JobQueue.shared.add(GroupAvatarDownloadJob(openGroup.room, openGroup.server))
|
||||
}
|
||||
Log.d(KEY, "onOpenGroupAdded(${openGroup.server})")
|
||||
storage.onOpenGroupAdded(openGroup.server)
|
||||
|
@ -14,10 +14,9 @@ class GroupAvatarDownloadJob(val room: String, val server: String) : Job {
|
||||
|
||||
override fun execute() {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val imageId = storage.getOpenGroup(room, server)?.imageId ?: return
|
||||
try {
|
||||
val info = OpenGroupApi.getRoomInfo(room, server).get()
|
||||
val imageId = info.imageId ?: return
|
||||
val bytes = OpenGroupApi.downloadOpenGroupProfilePicture(server, info.token, imageId).get()
|
||||
val bytes = OpenGroupApi.downloadOpenGroupProfilePicture(server, room, imageId).get()
|
||||
val groupId = GroupUtil.getEncodedOpenGroupID("$server.$room".toByteArray())
|
||||
storage.updateProfilePicture(groupId, bytes)
|
||||
storage.updateTimestampUpdated(groupId, System.currentTimeMillis())
|
||||
|
@ -11,15 +11,17 @@ data class OpenGroup(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val publicKey: String,
|
||||
val imageId: String?,
|
||||
val infoUpdates: Int,
|
||||
) {
|
||||
|
||||
constructor(server: String, room: String, name: String, infoUpdates: Int, publicKey: String) : this(
|
||||
constructor(server: String, room: String, publicKey: String, name: String, imageId: String?, infoUpdates: Int) : this(
|
||||
server = server,
|
||||
room = room,
|
||||
id = "$server.$room",
|
||||
name = name,
|
||||
publicKey = publicKey,
|
||||
imageId = imageId,
|
||||
infoUpdates = infoUpdates,
|
||||
)
|
||||
|
||||
@ -31,11 +33,12 @@ data class OpenGroup(
|
||||
if (!json.has("room")) return null
|
||||
val room = json.get("room").asText().toLowerCase(Locale.US)
|
||||
val server = json.get("server").asText().toLowerCase(Locale.US)
|
||||
val displayName = json.get("displayName").asText()
|
||||
val publicKey = json.get("publicKey").asText()
|
||||
val displayName = json.get("displayName").asText()
|
||||
val imageId = json.get("imageId")?.asText()
|
||||
val infoUpdates = json.get("infoUpdates")?.asText()?.toIntOrNull() ?: 0
|
||||
val capabilities = json.get("capabilities")?.asText()?.split(",") ?: emptyList()
|
||||
OpenGroup(server, room, displayName, infoUpdates, publicKey)
|
||||
OpenGroup(server, room, displayName, publicKey, imageId, infoUpdates)
|
||||
} catch (e: Exception) {
|
||||
Log.w("Loki", "Couldn't parse open group from JSON: $jsonAsString.", e);
|
||||
null
|
||||
@ -53,11 +56,12 @@ data class OpenGroup(
|
||||
}
|
||||
}
|
||||
|
||||
fun toJson(): Map<String,String> = mapOf(
|
||||
fun toJson(): Map<String,String?> = mapOf(
|
||||
"room" to room,
|
||||
"server" to server,
|
||||
"displayName" to name,
|
||||
"publicKey" to publicKey,
|
||||
"displayName" to name,
|
||||
"imageId" to imageId,
|
||||
"infoUpdates" to infoUpdates.toString(),
|
||||
)
|
||||
|
||||
|
@ -91,7 +91,7 @@ object OpenGroupApi {
|
||||
val created: Long = 0,
|
||||
val activeUsers: Int = 0,
|
||||
val activeUsersCutoff: Int = 0,
|
||||
val imageId: Long? = null,
|
||||
val imageId: String? = null,
|
||||
val pinnedMessages: List<PinnedMessage> = emptyList(),
|
||||
val admin: Boolean = false,
|
||||
val globalAdmin: Boolean = false,
|
||||
@ -337,7 +337,7 @@ object OpenGroupApi {
|
||||
.plus(request.verb.rawValue.toByteArray())
|
||||
.plus("/${request.endpoint.value}".toByteArray())
|
||||
.plus(bodyHash)
|
||||
if (serverCapabilities.contains(Capability.BLIND.name.lowercase())) {
|
||||
if (serverCapabilities.isEmpty() || serverCapabilities.contains(Capability.BLIND.name.lowercase())) {
|
||||
SodiumUtilities.blindedKeyPair(publicKey, ed25519KeyPair)?.let { keyPair ->
|
||||
pubKey = SessionId(
|
||||
IdPrefix.BLINDED,
|
||||
@ -395,13 +395,13 @@ object OpenGroupApi {
|
||||
fun downloadOpenGroupProfilePicture(
|
||||
server: String,
|
||||
roomID: String,
|
||||
imageId: Long
|
||||
imageId: String
|
||||
): Promise<ByteArray, Exception> {
|
||||
val request = Request(
|
||||
verb = GET,
|
||||
room = roomID,
|
||||
server = server,
|
||||
endpoint = Endpoint.RoomFileIndividual(roomID, imageId.toString())
|
||||
endpoint = Endpoint.RoomFileIndividual(roomID, imageId)
|
||||
)
|
||||
return getResponseBody(request)
|
||||
}
|
||||
@ -794,16 +794,14 @@ object OpenGroupApi {
|
||||
|
||||
private fun sequentialBatch(
|
||||
server: String,
|
||||
requests: MutableList<BatchRequestInfo<*>>,
|
||||
authRequired: Boolean = true
|
||||
requests: MutableList<BatchRequestInfo<*>>
|
||||
): Promise<List<BatchResponse<*>>, Exception> {
|
||||
val request = Request(
|
||||
verb = POST,
|
||||
room = null,
|
||||
server = server,
|
||||
endpoint = Endpoint.Sequence,
|
||||
parameters = requests.map { it.request },
|
||||
isAuthRequired = authRequired
|
||||
parameters = requests.map { it.request }
|
||||
)
|
||||
return getBatchResponseJson(request, requests)
|
||||
}
|
||||
@ -912,8 +910,7 @@ object OpenGroupApi {
|
||||
|
||||
fun getCapabilitiesAndRoomInfo(
|
||||
room: String,
|
||||
server: String,
|
||||
authRequired: Boolean = true
|
||||
server: String
|
||||
): Promise<Pair<Capabilities, RoomInfo>, Exception> {
|
||||
val requests = mutableListOf<BatchRequestInfo<*>>(
|
||||
BatchRequestInfo(
|
||||
@ -933,7 +930,7 @@ object OpenGroupApi {
|
||||
responseType = object : TypeReference<RoomInfo>(){}
|
||||
)
|
||||
)
|
||||
return sequentialBatch(server, requests, authRequired).map {
|
||||
return sequentialBatch(server, requests).map {
|
||||
val capabilities = it.firstOrNull()?.body as? Capabilities ?: throw Error.ParsingFailed
|
||||
val roomInfo = it.lastOrNull()?.body as? RoomInfo ?: throw Error.ParsingFailed
|
||||
capabilities to roomInfo
|
||||
|
@ -59,7 +59,7 @@ class OpenGroupPoller(private val server: String, private val executorService: S
|
||||
fun poll(isPostCapabilitiesRetry: Boolean = false): Promise<Unit, Exception> {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val rooms = storage.getAllOpenGroups().values.filter { it.server == server }.map { it.room }
|
||||
rooms.forEach { downloadGroupAvatarIfNeeded(it) }
|
||||
|
||||
return OpenGroupApi.poll(rooms, server).successBackground { responses ->
|
||||
responses.filterNot { it.body == null }.forEach { response ->
|
||||
when (response.endpoint) {
|
||||
@ -123,9 +123,10 @@ class OpenGroupPoller(private val server: String, private val executorService: S
|
||||
val openGroup = OpenGroup(
|
||||
server = server,
|
||||
room = pollInfo.token,
|
||||
name = pollInfo.details?.name ?: "",
|
||||
infoUpdates = pollInfo.details?.infoUpdates ?: 0,
|
||||
name = if (pollInfo.details != null) { pollInfo.details.name } else { existingOpenGroup.name },
|
||||
infoUpdates = if (pollInfo.details != null) { pollInfo.details.infoUpdates } else { existingOpenGroup.infoUpdates },
|
||||
publicKey = publicKey,
|
||||
imageId = if (pollInfo.details != null) { pollInfo.details.imageId } else { existingOpenGroup.imageId }
|
||||
)
|
||||
// - Open Group changes
|
||||
storage.updateOpenGroup(openGroup)
|
||||
@ -155,6 +156,11 @@ class OpenGroupPoller(private val server: String, private val executorService: S
|
||||
GroupMember(groupId, it, GroupMemberRole.HIDDEN_ADMIN)
|
||||
})
|
||||
}
|
||||
|
||||
// Start downloading the room image (if we don't have one or it's been updated)
|
||||
if (pollInfo.details?.imageId != null && pollInfo.details.imageId != existingOpenGroup.imageId) {
|
||||
JobQueue.shared.add(GroupAvatarDownloadJob(roomToken, server))
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleMessages(
|
||||
@ -284,16 +290,4 @@ class OpenGroupPoller(private val server: String, private val executorService: S
|
||||
JobQueue.shared.add(deleteJob)
|
||||
}
|
||||
}
|
||||
|
||||
private fun downloadGroupAvatarIfNeeded(room: String) {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
if (storage.getGroupAvatarDownloadJob(server, room) != null) return
|
||||
val groupId = GroupUtil.getEncodedOpenGroupID("$server.$room".toByteArray())
|
||||
storage.getGroup(groupId)?.let {
|
||||
if (System.currentTimeMillis() > it.updatedTimestamp + TimeUnit.DAYS.toMillis(7)) {
|
||||
JobQueue.shared.add(GroupAvatarDownloadJob(room, server))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -16,8 +16,10 @@ interface LokiAPIDatabaseProtocol {
|
||||
fun setSwarm(publicKey: String, newValue: Set<Snode>)
|
||||
fun getLastMessageHashValue(snode: Snode, publicKey: String, namespace: Int): String?
|
||||
fun setLastMessageHashValue(snode: Snode, publicKey: String, newValue: String, namespace: Int)
|
||||
fun clearAllLastMessageHashes()
|
||||
fun getReceivedMessageHashValues(publicKey: String, namespace: Int): Set<String>?
|
||||
fun setReceivedMessageHashValues(publicKey: String, newValue: Set<String>, namespace: Int)
|
||||
fun clearReceivedMessageHashValues()
|
||||
fun getAuthToken(server: String): String?
|
||||
fun setAuthToken(server: String, newValue: String?)
|
||||
fun setUserCount(room: String, server: String, newValue: Int)
|
||||
|
Loading…
Reference in New Issue
Block a user