mirror of
https://github.com/oxen-io/session-android.git
synced 2025-01-12 00:03:54 +00:00
Migrate old official open group locations for polling and adding (#932)
* feat: adding in first part of open group migrations and tests for migration logic / helpers * feat: test code and migration logic for open groups in the case of no conflicts * feat: add in extra test cases and refactor code for migrator * refactor: migrate open group join URLs and references to server in adding new open groups to catch legacy and re-write it * refactor: joining open groups using OpenGroupUrlParser.kt now * fix: add in compile issues for renamed OpenGroupApi.kt from OpenGroupV2 * fix: prevent duplicates of http/https for new open group DNS and prevent adding new groups based on public key * fix: room and server swapped parameters * fix: replace default server for config messages * fix: actually using public key to de-dupe didn't work for rooms * build: bump version code and name
This commit is contained in:
parent
5469f232a0
commit
d6d0c52745
@ -159,8 +159,8 @@ dependencies {
|
|||||||
testImplementation 'org.robolectric:shadows-multidex:4.4'
|
testImplementation 'org.robolectric:shadows-multidex:4.4'
|
||||||
}
|
}
|
||||||
|
|
||||||
def canonicalVersionCode = 293
|
def canonicalVersionCode = 294
|
||||||
def canonicalVersionName = "1.14.1"
|
def canonicalVersionName = "1.14.2"
|
||||||
|
|
||||||
def postFixSize = 10
|
def postFixSize = 10
|
||||||
def abiPostFix = ['armeabi-v7a' : 1,
|
def abiPostFix = ['armeabi-v7a' : 1,
|
||||||
|
@ -58,6 +58,7 @@ import org.thoughtcrime.securesms.database.Storage;
|
|||||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
|
import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
|
||||||
import org.thoughtcrime.securesms.dependencies.DatabaseModule;
|
import org.thoughtcrime.securesms.dependencies.DatabaseModule;
|
||||||
import org.thoughtcrime.securesms.groups.OpenGroupManager;
|
import org.thoughtcrime.securesms.groups.OpenGroupManager;
|
||||||
|
import org.thoughtcrime.securesms.groups.OpenGroupMigrator;
|
||||||
import org.thoughtcrime.securesms.home.HomeActivity;
|
import org.thoughtcrime.securesms.home.HomeActivity;
|
||||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||||
import org.thoughtcrime.securesms.jobmanager.impl.JsonDataSerializer;
|
import org.thoughtcrime.securesms.jobmanager.impl.JsonDataSerializer;
|
||||||
@ -191,6 +192,9 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
|||||||
storage,
|
storage,
|
||||||
messageDataProvider,
|
messageDataProvider,
|
||||||
()-> KeyPairUtilities.INSTANCE.getUserED25519KeyPair(this));
|
()-> KeyPairUtilities.INSTANCE.getUserED25519KeyPair(this));
|
||||||
|
// migrate session open group data
|
||||||
|
OpenGroupMigrator.migrate(getDatabaseComponent());
|
||||||
|
// end migration
|
||||||
callMessageProcessor = new CallMessageProcessor(this, textSecurePreferences, ProcessLifecycleOwner.get().getLifecycle(), storage);
|
callMessageProcessor = new CallMessageProcessor(this, textSecurePreferences, ProcessLifecycleOwner.get().getLifecycle(), storage);
|
||||||
Log.i(TAG, "onCreate()");
|
Log.i(TAG, "onCreate()");
|
||||||
startKovenant();
|
startKovenant();
|
||||||
|
@ -40,7 +40,7 @@ class JoinOpenGroupDialog(private val name: String, private val url: String) : B
|
|||||||
ThreadUtils.queue {
|
ThreadUtils.queue {
|
||||||
try {
|
try {
|
||||||
OpenGroupManager.add(openGroup.server, openGroup.room, openGroup.serverPublicKey, activity)
|
OpenGroupManager.add(openGroup.server, openGroup.room, openGroup.serverPublicKey, activity)
|
||||||
MessagingModuleConfiguration.shared.storage.onOpenGroupAdded(url)
|
MessagingModuleConfiguration.shared.storage.onOpenGroupAdded(openGroup.server)
|
||||||
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(activity)
|
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(activity)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Toast.makeText(activity, R.string.activity_join_public_chat_error, Toast.LENGTH_SHORT).show()
|
Toast.makeText(activity, R.string.activity_join_public_chat_error, Toast.LENGTH_SHORT).show()
|
||||||
|
@ -14,6 +14,7 @@ import com.annimon.stream.Stream;
|
|||||||
|
|
||||||
import net.sqlcipher.database.SQLiteDatabase;
|
import net.sqlcipher.database.SQLiteDatabase;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.session.libsession.utilities.Address;
|
import org.session.libsession.utilities.Address;
|
||||||
import org.session.libsession.utilities.GroupRecord;
|
import org.session.libsession.utilities.GroupRecord;
|
||||||
import org.session.libsession.utilities.TextSecurePreferences;
|
import org.session.libsession.utilities.TextSecurePreferences;
|
||||||
@ -441,7 +442,15 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class Reader implements Closeable {
|
public void migrateEncodedGroup(@NotNull String legacyEncodedGroupId, @NotNull String newEncodedGroupId) {
|
||||||
|
String query = GROUP_ID+" = ?";
|
||||||
|
ContentValues contentValues = new ContentValues(1);
|
||||||
|
contentValues.put(GROUP_ID, newEncodedGroupId);
|
||||||
|
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||||
|
db.update(TABLE_NAME, contentValues, query, new String[]{legacyEncodedGroupId});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Reader implements Closeable {
|
||||||
|
|
||||||
private final Cursor cursor;
|
private final Cursor cursor;
|
||||||
|
|
||||||
|
@ -380,18 +380,29 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
|
|||||||
database.delete(lastDeletionServerIDTable, "$lastDeletionServerIDTableIndex = ?", wrap(index))
|
database.delete(lastDeletionServerIDTable, "$lastDeletionServerIDTableIndex = ?", wrap(index))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeLastDeletionServerID(group: Long, server: String) {
|
override fun migrateLegacyOpenGroup(legacyServerId: String, newServerId: String) {
|
||||||
val database = databaseHelper.writableDatabase
|
val database = databaseHelper.writableDatabase
|
||||||
val index = "$server.$group"
|
database.beginTransaction()
|
||||||
database.delete(lastDeletionServerIDTable,"$lastDeletionServerIDTableIndex = ?", wrap(index))
|
val authRow = wrap(mapOf(server to newServerId))
|
||||||
}
|
database.update(openGroupAuthTokenTable, authRow, "$server = ?", wrap(legacyServerId))
|
||||||
|
val lastMessageRow = wrap(mapOf(lastMessageServerIDTableIndex to newServerId))
|
||||||
fun getUserCount(group: Long, server: String): Int? {
|
database.update(lastMessageServerIDTable, lastMessageRow,
|
||||||
val database = databaseHelper.readableDatabase
|
"$lastMessageServerIDTableIndex = ?", wrap(legacyServerId))
|
||||||
val index = "$server.$group"
|
val lastDeletionRow = wrap(mapOf(lastDeletionServerIDTableIndex to newServerId))
|
||||||
return database.get(userCountTable, "$publicChatID = ?", wrap(index)) { cursor ->
|
database.update(
|
||||||
cursor.getInt(userCount)
|
lastDeletionServerIDTable, lastDeletionRow,
|
||||||
}?.toInt()
|
"$lastDeletionServerIDTableIndex = ?", wrap(legacyServerId))
|
||||||
|
val userCountRow = wrap(mapOf(publicChatID to newServerId))
|
||||||
|
database.update(
|
||||||
|
userCountTable, userCountRow,
|
||||||
|
"$publicChatID = ?", wrap(legacyServerId)
|
||||||
|
)
|
||||||
|
val publicKeyRow = wrap(mapOf(server to newServerId))
|
||||||
|
database.update(
|
||||||
|
openGroupPublicKeyTable, publicKeyRow,
|
||||||
|
"$server = ?", wrap(legacyServerId)
|
||||||
|
)
|
||||||
|
database.endTransaction()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getUserCount(room: String, server: String): Int? {
|
fun getUserCount(room: String, server: String): Int? {
|
||||||
@ -402,13 +413,6 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
|
|||||||
}?.toInt()
|
}?.toInt()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setUserCount(group: Long, server: String, newValue: Int) {
|
|
||||||
val database = databaseHelper.writableDatabase
|
|
||||||
val index = "$server.$group"
|
|
||||||
val row = wrap(mapOf( publicChatID to index, Companion.userCount to newValue.toString() ))
|
|
||||||
database.insertOrUpdate(userCountTable, row, "$publicChatID = ?", wrap(index))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setUserCount(room: String, server: String, newValue: Int) {
|
override fun setUserCount(room: String, server: String, newValue: Int) {
|
||||||
val database = databaseHelper.writableDatabase
|
val database = databaseHelper.writableDatabase
|
||||||
val index = "$server.$room"
|
val index = "$server.$room"
|
||||||
|
@ -177,4 +177,12 @@ class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Datab
|
|||||||
val database = databaseHelper.writableDatabase
|
val database = databaseHelper.writableDatabase
|
||||||
database.delete(messageHashTable, "${Companion.messageID} = ?", arrayOf(messageID.toString()))
|
database.delete(messageHashTable, "${Companion.messageID} = ?", arrayOf(messageID.toString()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun migrateThreadId(legacyThreadId: Long, newThreadId: Long) {
|
||||||
|
val database = databaseHelper.writableDatabase
|
||||||
|
val contentValues = ContentValues(1)
|
||||||
|
contentValues.put(threadID, newThreadId)
|
||||||
|
database.update(messageThreadMappingTable, contentValues, "$threadID = ?", arrayOf(legacyThreadId.toString()))
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -7,15 +7,14 @@ import android.text.TextUtils;
|
|||||||
|
|
||||||
import net.sqlcipher.database.SQLiteDatabase;
|
import net.sqlcipher.database.SQLiteDatabase;
|
||||||
|
|
||||||
|
import org.session.libsession.utilities.Address;
|
||||||
import org.session.libsession.utilities.Document;
|
import org.session.libsession.utilities.Document;
|
||||||
import org.session.libsession.utilities.IdentityKeyMismatch;
|
import org.session.libsession.utilities.IdentityKeyMismatch;
|
||||||
import org.session.libsession.utilities.IdentityKeyMismatchList;
|
import org.session.libsession.utilities.IdentityKeyMismatchList;
|
||||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
|
||||||
import org.session.libsignal.utilities.Log;
|
|
||||||
import org.session.libsignal.crypto.IdentityKey;
|
import org.session.libsignal.crypto.IdentityKey;
|
||||||
|
|
||||||
import org.session.libsession.utilities.Address;
|
|
||||||
import org.session.libsignal.utilities.JsonUtil;
|
import org.session.libsignal.utilities.JsonUtil;
|
||||||
|
import org.session.libsignal.utilities.Log;
|
||||||
|
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@ -159,6 +158,15 @@ public abstract class MessagingDatabase extends Database implements MmsSmsColumn
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void migrateThreadId(long oldThreadId, long newThreadId) {
|
||||||
|
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||||
|
String where = THREAD_ID+" = ?";
|
||||||
|
String[] args = new String[]{oldThreadId+""};
|
||||||
|
ContentValues contentValues = new ContentValues();
|
||||||
|
contentValues.put(THREAD_ID, newThreadId);
|
||||||
|
db.update(getTableName(), contentValues, where, args);
|
||||||
|
}
|
||||||
|
|
||||||
public static class SyncMessageId {
|
public static class SyncMessageId {
|
||||||
|
|
||||||
private final Address address;
|
private final Address address;
|
||||||
|
@ -567,9 +567,8 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
OpenGroupManager.addOpenGroup(urlAsString, context)
|
OpenGroupManager.addOpenGroup(urlAsString, context)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOpenGroupAdded(urlAsString: String) {
|
override fun onOpenGroupAdded(server: String) {
|
||||||
val server = OpenGroup.getServer(urlAsString)
|
OpenGroupManager.restartPollerForServer(server.removeSuffix("/"))
|
||||||
OpenGroupManager.restartPollerForServer(server.toString().removeSuffix("/"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hasBackgroundGroupAddJob(groupJoinUrl: String): Boolean {
|
override fun hasBackgroundGroupAddJob(groupJoinUrl: String): Boolean {
|
||||||
|
@ -34,6 +34,7 @@ import com.annimon.stream.Stream;
|
|||||||
|
|
||||||
import net.sqlcipher.database.SQLiteDatabase;
|
import net.sqlcipher.database.SQLiteDatabase;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.session.libsession.utilities.Address;
|
import org.session.libsession.utilities.Address;
|
||||||
import org.session.libsession.utilities.Contact;
|
import org.session.libsession.utilities.Contact;
|
||||||
import org.session.libsession.utilities.DelimiterUtil;
|
import org.session.libsession.utilities.DelimiterUtil;
|
||||||
@ -56,12 +57,15 @@ import org.thoughtcrime.securesms.database.model.MessageRecord;
|
|||||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||||
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
||||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
|
import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
|
||||||
|
import org.thoughtcrime.securesms.groups.OpenGroupMigrator;
|
||||||
import org.thoughtcrime.securesms.mms.Slide;
|
import org.thoughtcrime.securesms.mms.Slide;
|
||||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||||
import org.thoughtcrime.securesms.notifications.MarkReadReceiver;
|
import org.thoughtcrime.securesms.notifications.MarkReadReceiver;
|
||||||
import org.thoughtcrime.securesms.util.SessionMetaProtocol;
|
import org.thoughtcrime.securesms.util.SessionMetaProtocol;
|
||||||
|
|
||||||
import java.io.Closeable;
|
import java.io.Closeable;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -765,7 +769,85 @@ public class ThreadDatabase extends Database {
|
|||||||
return query;
|
return query;
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface ProgressListener {
|
@NotNull
|
||||||
|
public List<ThreadRecord> getHttpOxenOpenGroups() {
|
||||||
|
String where = TABLE_NAME+"."+ADDRESS+" LIKE ?";
|
||||||
|
String selection = OpenGroupMigrator.HTTP_PREFIX+OpenGroupMigrator.OPEN_GET_SESSION_TRAILING_DOT_ENCODED +"%";
|
||||||
|
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||||
|
String query = createQuery(where, 0);
|
||||||
|
Cursor cursor = db.rawQuery(query, new String[]{selection});
|
||||||
|
|
||||||
|
if (cursor == null) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
List<ThreadRecord> threads = new ArrayList<>();
|
||||||
|
try {
|
||||||
|
Reader reader = readerFor(cursor);
|
||||||
|
ThreadRecord record;
|
||||||
|
while ((record = reader.getNext()) != null) {
|
||||||
|
threads.add(record);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
cursor.close();
|
||||||
|
}
|
||||||
|
return threads;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
public List<ThreadRecord> getLegacyOxenOpenGroups() {
|
||||||
|
String where = TABLE_NAME+"."+ADDRESS+" LIKE ?";
|
||||||
|
String selection = OpenGroupMigrator.LEGACY_GROUP_ENCODED_ID+"%";
|
||||||
|
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||||
|
String query = createQuery(where, 0);
|
||||||
|
Cursor cursor = db.rawQuery(query, new String[]{selection});
|
||||||
|
|
||||||
|
if (cursor == null) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
List<ThreadRecord> threads = new ArrayList<>();
|
||||||
|
try {
|
||||||
|
Reader reader = readerFor(cursor);
|
||||||
|
ThreadRecord record;
|
||||||
|
while ((record = reader.getNext()) != null) {
|
||||||
|
threads.add(record);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
cursor.close();
|
||||||
|
}
|
||||||
|
return threads;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
public List<ThreadRecord> getHttpsOxenOpenGroups() {
|
||||||
|
String where = TABLE_NAME+"."+ADDRESS+" LIKE ?";
|
||||||
|
String selection = OpenGroupMigrator.NEW_GROUP_ENCODED_ID+"%";
|
||||||
|
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||||
|
String query = createQuery(where, 0);
|
||||||
|
Cursor cursor = db.rawQuery(query, new String[]{selection});
|
||||||
|
if (cursor == null) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
List<ThreadRecord> threads = new ArrayList<>();
|
||||||
|
try {
|
||||||
|
Reader reader = readerFor(cursor);
|
||||||
|
ThreadRecord record;
|
||||||
|
while ((record = reader.getNext()) != null) {
|
||||||
|
threads.add(record);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
cursor.close();
|
||||||
|
}
|
||||||
|
return threads;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void migrateEncodedGroup(long threadId, @NotNull String newEncodedGroupId) {
|
||||||
|
ContentValues contentValues = new ContentValues(1);
|
||||||
|
contentValues.put(ADDRESS, newEncodedGroupId);
|
||||||
|
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||||
|
db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {threadId+""});
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface ProgressListener {
|
||||||
void onProgress(int complete, int total);
|
void onProgress(int complete, int total);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,14 +24,14 @@ import kotlinx.coroutines.withContext
|
|||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import network.loki.messenger.databinding.ActivityJoinPublicChatBinding
|
import network.loki.messenger.databinding.ActivityJoinPublicChatBinding
|
||||||
import network.loki.messenger.databinding.FragmentEnterChatUrlBinding
|
import network.loki.messenger.databinding.FragmentEnterChatUrlBinding
|
||||||
import okhttp3.HttpUrl
|
|
||||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupApi.DefaultGroup
|
import org.session.libsession.messaging.open_groups.OpenGroupApi.DefaultGroup
|
||||||
import org.session.libsession.utilities.Address
|
import org.session.libsession.utilities.Address
|
||||||
import org.session.libsession.utilities.GroupUtil
|
import org.session.libsession.utilities.GroupUtil
|
||||||
|
import org.session.libsession.utilities.OpenGroupUrlParser
|
||||||
|
import org.session.libsession.utilities.OpenGroupUrlParser.Error
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
import org.session.libsignal.utilities.Log
|
import org.session.libsignal.utilities.Log
|
||||||
import org.session.libsignal.utilities.PublicKeyValidation
|
|
||||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
||||||
@ -83,32 +83,27 @@ class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCode
|
|||||||
|
|
||||||
fun joinPublicChatIfPossible(url: String) {
|
fun joinPublicChatIfPossible(url: String) {
|
||||||
// Add "http" if not entered explicitly
|
// Add "http" if not entered explicitly
|
||||||
val stringWithExplicitScheme = if (!url.startsWith("http")) "http://$url" else url
|
val openGroup = try {
|
||||||
val url = HttpUrl.parse(stringWithExplicitScheme) ?: return Toast.makeText(this,R.string.invalid_url, Toast.LENGTH_SHORT).show()
|
OpenGroupUrlParser.parseUrl(url)
|
||||||
val room = url.pathSegments().firstOrNull()
|
} catch (e: Error) {
|
||||||
val publicKey = url.queryParameter("public_key")
|
when (e) {
|
||||||
val isV2OpenGroup = !room.isNullOrEmpty()
|
is Error.MalformedURL -> return Toast.makeText(this, R.string.activity_join_public_chat_error, Toast.LENGTH_SHORT).show()
|
||||||
if (isV2OpenGroup && (publicKey == null || !PublicKeyValidation.isValid(publicKey, 64,false))) {
|
is Error.InvalidPublicKey -> return Toast.makeText(this, R.string.invalid_public_key, Toast.LENGTH_SHORT).show()
|
||||||
return Toast.makeText(this, R.string.invalid_public_key, Toast.LENGTH_SHORT).show()
|
is Error.NoPublicKey -> return Toast.makeText(this, R.string.invalid_public_key, Toast.LENGTH_SHORT).show()
|
||||||
|
is Error.NoRoom -> return Toast.makeText(this, R.string.activity_join_public_chat_error, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
showLoader()
|
showLoader()
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val (threadID, groupID) = if (isV2OpenGroup) {
|
val sanitizedServer = openGroup.server.removeSuffix("/")
|
||||||
val server = HttpUrl.Builder().scheme(url.scheme()).host(url.host()).apply {
|
val openGroupID = "$sanitizedServer.${openGroup.room}"
|
||||||
if (url.port() != 80 || url.port() != 443) { this.port(url.port()) } // Non-standard port; add to server
|
OpenGroupManager.add(sanitizedServer, openGroup.room, openGroup.serverPublicKey, this@JoinPublicChatActivity)
|
||||||
}.build()
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
|
storage.onOpenGroupAdded(sanitizedServer)
|
||||||
|
val threadID = GroupManager.getOpenGroupThreadID(openGroupID, this@JoinPublicChatActivity)
|
||||||
|
val groupID = GroupUtil.getEncodedOpenGroupID(openGroupID.toByteArray())
|
||||||
|
|
||||||
val sanitizedServer = server.toString().removeSuffix("/")
|
|
||||||
val openGroupID = "$sanitizedServer.${room!!}"
|
|
||||||
OpenGroupManager.add(sanitizedServer, room, publicKey!!, this@JoinPublicChatActivity)
|
|
||||||
MessagingModuleConfiguration.shared.storage.onOpenGroupAdded(stringWithExplicitScheme)
|
|
||||||
val threadID = GroupManager.getOpenGroupThreadID(openGroupID, this@JoinPublicChatActivity)
|
|
||||||
val groupID = GroupUtil.getEncodedOpenGroupID(openGroupID.toByteArray())
|
|
||||||
threadID to groupID
|
|
||||||
} else {
|
|
||||||
throw Exception("No longer supported.")
|
|
||||||
}
|
|
||||||
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(this@JoinPublicChatActivity)
|
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(this@JoinPublicChatActivity)
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
val recipient = Recipient.from(this@JoinPublicChatActivity, Address.fromSerialized(groupID), false)
|
val recipient = Recipient.from(this@JoinPublicChatActivity, Address.fromSerialized(groupID), false)
|
||||||
|
@ -8,6 +8,7 @@ import org.session.libsession.messaging.open_groups.GroupMemberRole
|
|||||||
import org.session.libsession.messaging.open_groups.OpenGroup
|
import org.session.libsession.messaging.open_groups.OpenGroup
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupApi
|
import org.session.libsession.messaging.open_groups.OpenGroupApi
|
||||||
import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPoller
|
import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPoller
|
||||||
|
import org.session.libsignal.utilities.Log
|
||||||
import org.session.libsignal.utilities.ThreadUtils
|
import org.session.libsignal.utilities.ThreadUtils
|
||||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
@ -71,7 +72,7 @@ object OpenGroupManager {
|
|||||||
storage.removeLastInboxMessageId(server)
|
storage.removeLastInboxMessageId(server)
|
||||||
storage.removeLastOutboxMessageId(server)
|
storage.removeLastOutboxMessageId(server)
|
||||||
// Store the public key
|
// Store the public key
|
||||||
storage.setOpenGroupPublicKey(server,publicKey)
|
storage.setOpenGroupPublicKey(server, publicKey)
|
||||||
// Get capabilities
|
// Get capabilities
|
||||||
val capabilities = OpenGroupApi.getCapabilities(server).get()
|
val capabilities = OpenGroupApi.getCapabilities(server).get()
|
||||||
storage.setServerCapabilities(server, capabilities.capabilities)
|
storage.setServerCapabilities(server, capabilities.capabilities)
|
||||||
@ -92,6 +93,7 @@ object OpenGroupManager {
|
|||||||
pollers[server]?.stop()
|
pollers[server]?.stop()
|
||||||
pollers[server]?.startIfNeeded() ?: run {
|
pollers[server]?.startIfNeeded() ?: run {
|
||||||
val poller = OpenGroupPoller(server, executorService)
|
val poller = OpenGroupPoller(server, executorService)
|
||||||
|
Log.d("Loki", "Starting poller for open group: $server")
|
||||||
pollers[server] = poller
|
pollers[server] = poller
|
||||||
poller.startIfNeeded()
|
poller.startIfNeeded()
|
||||||
}
|
}
|
||||||
@ -133,7 +135,7 @@ object OpenGroupManager {
|
|||||||
val server = OpenGroup.getServer(urlAsString)
|
val server = OpenGroup.getServer(urlAsString)
|
||||||
val room = url.pathSegments().firstOrNull() ?: return
|
val room = url.pathSegments().firstOrNull() ?: return
|
||||||
val publicKey = url.queryParameter("public_key") ?: return
|
val publicKey = url.queryParameter("public_key") ?: return
|
||||||
add(server.toString().removeSuffix("/"), room, publicKey, context)
|
add(server.toString().removeSuffix("/"), room, publicKey, context) // assume migrated from calling function
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateOpenGroup(openGroup: OpenGroup, context: Context) {
|
fun updateOpenGroup(openGroup: OpenGroup, context: Context) {
|
||||||
|
@ -0,0 +1,139 @@
|
|||||||
|
package org.thoughtcrime.securesms.groups
|
||||||
|
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
|
import org.session.libsession.messaging.open_groups.OpenGroupApi
|
||||||
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
|
import org.session.libsignal.utilities.Hex
|
||||||
|
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
||||||
|
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||||
|
|
||||||
|
object OpenGroupMigrator {
|
||||||
|
const val HTTP_PREFIX = "__loki_public_chat_group__!687474703a2f2f"
|
||||||
|
private const val HTTPS_PREFIX = "__loki_public_chat_group__!68747470733a2f2f"
|
||||||
|
const val OPEN_GET_SESSION_TRAILING_DOT_ENCODED = "6f70656e2e67657473657373696f6e2e6f72672e"
|
||||||
|
const val LEGACY_GROUP_ENCODED_ID = "__loki_public_chat_group__!687474703a2f2f3131362e3230332e37302e33332e" // old IP based toByteArray()
|
||||||
|
const val NEW_GROUP_ENCODED_ID = "__loki_public_chat_group__!68747470733a2f2f6f70656e2e67657473657373696f6e2e6f72672e" // new URL based toByteArray()
|
||||||
|
|
||||||
|
data class OpenGroupMapping(val stub: String, val legacyThreadId: Long, val newThreadId: Long?)
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
fun Recipient.roomStub(): String? {
|
||||||
|
if (!isOpenGroupRecipient) return null
|
||||||
|
val serialized = address.serialize()
|
||||||
|
if (serialized.startsWith(LEGACY_GROUP_ENCODED_ID)) {
|
||||||
|
return serialized.replace(LEGACY_GROUP_ENCODED_ID,"")
|
||||||
|
} else if (serialized.startsWith(NEW_GROUP_ENCODED_ID)) {
|
||||||
|
return serialized.replace(NEW_GROUP_ENCODED_ID,"")
|
||||||
|
} else if (serialized.startsWith(HTTP_PREFIX + OPEN_GET_SESSION_TRAILING_DOT_ENCODED)) {
|
||||||
|
return serialized.replace(HTTP_PREFIX + OPEN_GET_SESSION_TRAILING_DOT_ENCODED, "")
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
fun getExistingMappings(legacy: List<ThreadRecord>, new: List<ThreadRecord>): List<OpenGroupMapping> {
|
||||||
|
val legacyStubsMapping = legacy.mapNotNull { thread ->
|
||||||
|
val stub = thread.recipient.roomStub()
|
||||||
|
stub?.let { it to thread.threadId }
|
||||||
|
}
|
||||||
|
val newStubsMapping = new.mapNotNull { thread ->
|
||||||
|
val stub = thread.recipient.roomStub()
|
||||||
|
stub?.let { it to thread.threadId }
|
||||||
|
}
|
||||||
|
return legacyStubsMapping.map { (legacyEncodedStub, legacyId) ->
|
||||||
|
// get 'new' open group thread ID if stubs match
|
||||||
|
OpenGroupMapping(
|
||||||
|
legacyEncodedStub,
|
||||||
|
legacyId,
|
||||||
|
newStubsMapping.firstOrNull { (newEncodedStub, _) -> newEncodedStub == legacyEncodedStub }?.second
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun migrate(databaseComponent: DatabaseComponent) {
|
||||||
|
// migrate thread db
|
||||||
|
val threadDb = databaseComponent.threadDatabase()
|
||||||
|
|
||||||
|
val legacyOpenGroups = threadDb.legacyOxenOpenGroups
|
||||||
|
val httpBasedNewGroups = threadDb.httpOxenOpenGroups
|
||||||
|
if (legacyOpenGroups.isEmpty() && httpBasedNewGroups.isEmpty()) return // no need to migrate
|
||||||
|
|
||||||
|
val newOpenGroups = threadDb.httpsOxenOpenGroups
|
||||||
|
val firstStepMigration = getExistingMappings(legacyOpenGroups, newOpenGroups)
|
||||||
|
|
||||||
|
val secondStepMigration = getExistingMappings(httpBasedNewGroups, newOpenGroups)
|
||||||
|
|
||||||
|
val groupDb = databaseComponent.groupDatabase()
|
||||||
|
val lokiApiDb = databaseComponent.lokiAPIDatabase()
|
||||||
|
val smsDb = databaseComponent.smsDatabase()
|
||||||
|
val mmsDb = databaseComponent.mmsDatabase()
|
||||||
|
val lokiMessageDatabase = databaseComponent.lokiMessageDatabase()
|
||||||
|
val lokiThreadDatabase = databaseComponent.lokiThreadDatabase()
|
||||||
|
|
||||||
|
firstStepMigration.forEach { (stub, old, new) ->
|
||||||
|
val legacyEncodedGroupId = LEGACY_GROUP_ENCODED_ID+stub
|
||||||
|
if (new == null) {
|
||||||
|
val newEncodedGroupId = NEW_GROUP_ENCODED_ID+stub
|
||||||
|
// migrate thread and group encoded values
|
||||||
|
threadDb.migrateEncodedGroup(old, newEncodedGroupId)
|
||||||
|
groupDb.migrateEncodedGroup(legacyEncodedGroupId, newEncodedGroupId)
|
||||||
|
// migrate Loki API DB values
|
||||||
|
// decode the hex to bytes, decode byte array to string i.e. "oxen" or "session"
|
||||||
|
val decodedStub = Hex.fromStringCondensed(stub).decodeToString()
|
||||||
|
val legacyLokiServerId = "${OpenGroupApi.legacyDefaultServer}.$decodedStub"
|
||||||
|
val newLokiServerId = "${OpenGroupApi.defaultServer}.$decodedStub"
|
||||||
|
lokiApiDb.migrateLegacyOpenGroup(legacyLokiServerId, newLokiServerId)
|
||||||
|
// migrate loki thread db server info
|
||||||
|
val oldServerInfo = lokiThreadDatabase.getOpenGroupChat(old)
|
||||||
|
val newServerInfo = oldServerInfo!!.copy(server = OpenGroupApi.defaultServer, id = newLokiServerId)
|
||||||
|
lokiThreadDatabase.setOpenGroupChat(newServerInfo, old)
|
||||||
|
} else {
|
||||||
|
// has a legacy and a new one
|
||||||
|
// migrate SMS and MMS tables
|
||||||
|
smsDb.migrateThreadId(old, new)
|
||||||
|
mmsDb.migrateThreadId(old, new)
|
||||||
|
lokiMessageDatabase.migrateThreadId(old, new)
|
||||||
|
// delete group for legacy ID
|
||||||
|
groupDb.delete(legacyEncodedGroupId)
|
||||||
|
// delete thread for legacy ID
|
||||||
|
threadDb.deleteConversation(old)
|
||||||
|
lokiThreadDatabase.removeOpenGroupChat(old)
|
||||||
|
}
|
||||||
|
// maybe migrate jobs here
|
||||||
|
}
|
||||||
|
|
||||||
|
secondStepMigration.forEach { (stub, old, new) ->
|
||||||
|
val legacyEncodedGroupId = HTTP_PREFIX + OPEN_GET_SESSION_TRAILING_DOT_ENCODED + stub
|
||||||
|
if (new == null) {
|
||||||
|
val newEncodedGroupId = NEW_GROUP_ENCODED_ID+stub
|
||||||
|
// migrate thread and group encoded values
|
||||||
|
threadDb.migrateEncodedGroup(old, newEncodedGroupId)
|
||||||
|
groupDb.migrateEncodedGroup(legacyEncodedGroupId, newEncodedGroupId)
|
||||||
|
// migrate Loki API DB values
|
||||||
|
// decode the hex to bytes, decode byte array to string i.e. "oxen" or "session"
|
||||||
|
val decodedStub = Hex.fromStringCondensed(stub).decodeToString()
|
||||||
|
val legacyLokiServerId = "${OpenGroupApi.httpDefaultServer}.$decodedStub"
|
||||||
|
val newLokiServerId = "${OpenGroupApi.defaultServer}.$decodedStub"
|
||||||
|
lokiApiDb.migrateLegacyOpenGroup(legacyLokiServerId, newLokiServerId)
|
||||||
|
// migrate loki thread db server info
|
||||||
|
val oldServerInfo = lokiThreadDatabase.getOpenGroupChat(old)
|
||||||
|
val newServerInfo = oldServerInfo!!.copy(server = OpenGroupApi.defaultServer, id = newLokiServerId)
|
||||||
|
lokiThreadDatabase.setOpenGroupChat(newServerInfo, old)
|
||||||
|
} else {
|
||||||
|
// has a legacy and a new one
|
||||||
|
// migrate SMS and MMS tables
|
||||||
|
smsDb.migrateThreadId(old, new)
|
||||||
|
mmsDb.migrateThreadId(old, new)
|
||||||
|
lokiMessageDatabase.migrateThreadId(old, new)
|
||||||
|
// delete group for legacy ID
|
||||||
|
groupDb.delete(legacyEncodedGroupId)
|
||||||
|
// delete thread for legacy ID
|
||||||
|
threadDb.deleteConversation(old)
|
||||||
|
lokiThreadDatabase.removeOpenGroupChat(old)
|
||||||
|
}
|
||||||
|
// maybe migrate jobs here
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -124,10 +124,10 @@
|
|||||||
android:id="@+id/snippetTextView"
|
android:id="@+id/snippetTextView"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:maxLines="1"
|
|
||||||
android:ellipsize="end"
|
android:ellipsize="end"
|
||||||
android:textSize="@dimen/medium_font_size"
|
android:maxLines="1"
|
||||||
android:textColor="@color/text"
|
android:textColor="@color/text"
|
||||||
|
android:textSize="@dimen/medium_font_size"
|
||||||
tools:text="Sorry, gotta go fight crime again" />
|
tools:text="Sorry, gotta go fight crime again" />
|
||||||
|
|
||||||
<org.thoughtcrime.securesms.conversation.v2.components.TypingIndicatorView
|
<org.thoughtcrime.securesms.conversation.v2.components.TypingIndicatorView
|
||||||
|
@ -0,0 +1,281 @@
|
|||||||
|
package org.thoughtcrime.securesms.util
|
||||||
|
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
import org.mockito.kotlin.KStubbing
|
||||||
|
import org.mockito.kotlin.argumentCaptor
|
||||||
|
import org.mockito.kotlin.doAnswer
|
||||||
|
import org.mockito.kotlin.doReturn
|
||||||
|
import org.mockito.kotlin.eq
|
||||||
|
import org.mockito.kotlin.mock
|
||||||
|
import org.mockito.kotlin.verify
|
||||||
|
import org.mockito.kotlin.verifyNoMoreInteractions
|
||||||
|
import org.session.libsession.messaging.open_groups.OpenGroup
|
||||||
|
import org.session.libsession.messaging.open_groups.OpenGroupApi
|
||||||
|
import org.session.libsession.utilities.Address
|
||||||
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
|
import org.thoughtcrime.securesms.database.GroupDatabase
|
||||||
|
import org.thoughtcrime.securesms.database.LokiAPIDatabase
|
||||||
|
import org.thoughtcrime.securesms.database.LokiMessageDatabase
|
||||||
|
import org.thoughtcrime.securesms.database.LokiThreadDatabase
|
||||||
|
import org.thoughtcrime.securesms.database.MmsDatabase
|
||||||
|
import org.thoughtcrime.securesms.database.SmsDatabase
|
||||||
|
import org.thoughtcrime.securesms.database.ThreadDatabase
|
||||||
|
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
||||||
|
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||||
|
import org.thoughtcrime.securesms.groups.OpenGroupMigrator
|
||||||
|
import org.thoughtcrime.securesms.groups.OpenGroupMigrator.OpenGroupMapping
|
||||||
|
import org.thoughtcrime.securesms.groups.OpenGroupMigrator.roomStub
|
||||||
|
|
||||||
|
class OpenGroupMigrationTests {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val EXAMPLE_LEGACY_ENCODED_OPEN_GROUP = "__loki_public_chat_group__!687474703a2f2f3131362e3230332e37302e33332e6f78656e"
|
||||||
|
const val EXAMPLE_NEW_ENCODED_OPEN_GROUP = "__loki_public_chat_group__!68747470733a2f2f6f70656e2e67657473657373696f6e2e6f72672e6f78656e"
|
||||||
|
const val OXEN_STUB_HEX = "6f78656e"
|
||||||
|
|
||||||
|
const val EXAMPLE_LEGACY_SERVER_ID = "http://116.203.70.33.oxen"
|
||||||
|
const val EXAMPLE_NEW_SERVER_ID = "https://open.getsession.org.oxen"
|
||||||
|
|
||||||
|
const val LEGACY_THREAD_ID = 1L
|
||||||
|
const val NEW_THREAD_ID = 2L
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun legacyOpenGroupRecipient(additionalMocks: ((KStubbing<Recipient>) -> Unit) ? = null) = mock<Recipient> {
|
||||||
|
on { address } doReturn Address.fromSerialized(EXAMPLE_LEGACY_ENCODED_OPEN_GROUP)
|
||||||
|
on { isOpenGroupRecipient } doReturn true
|
||||||
|
additionalMocks?.let { it(this) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun newOpenGroupRecipient(additionalMocks: ((KStubbing<Recipient>) -> Unit) ? = null) = mock<Recipient> {
|
||||||
|
on { address } doReturn Address.fromSerialized(EXAMPLE_NEW_ENCODED_OPEN_GROUP)
|
||||||
|
on { isOpenGroupRecipient } doReturn true
|
||||||
|
additionalMocks?.let { it(this) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun legacyThreadRecord(additionalRecipientMocks: ((KStubbing<Recipient>) -> Unit) ? = null, additionalThreadMocks: ((KStubbing<ThreadRecord>) -> Unit)? = null) = mock<ThreadRecord> {
|
||||||
|
val returnedRecipient = legacyOpenGroupRecipient(additionalRecipientMocks)
|
||||||
|
on { recipient } doReturn returnedRecipient
|
||||||
|
on { threadId } doReturn LEGACY_THREAD_ID
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun newThreadRecord(additionalRecipientMocks: ((KStubbing<Recipient>) -> Unit)? = null, additionalThreadMocks: ((KStubbing<ThreadRecord>) -> Unit)? = null) = mock<ThreadRecord> {
|
||||||
|
val returnedRecipient = newOpenGroupRecipient(additionalRecipientMocks)
|
||||||
|
on { recipient } doReturn returnedRecipient
|
||||||
|
on { threadId } doReturn NEW_THREAD_ID
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `it should generate the correct room stubs for legacy groups`() {
|
||||||
|
val mockRecipient = legacyOpenGroupRecipient()
|
||||||
|
assertEquals(OXEN_STUB_HEX, mockRecipient.roomStub())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `it should generate the correct room stubs for new groups`() {
|
||||||
|
val mockNewRecipient = newOpenGroupRecipient()
|
||||||
|
assertEquals(OXEN_STUB_HEX, mockNewRecipient.roomStub())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `it should return correct mappings`() {
|
||||||
|
val legacyThread = legacyThreadRecord()
|
||||||
|
val newThread = newThreadRecord()
|
||||||
|
|
||||||
|
val expectedMapping = listOf(
|
||||||
|
OpenGroupMapping(OXEN_STUB_HEX, LEGACY_THREAD_ID, NEW_THREAD_ID)
|
||||||
|
)
|
||||||
|
|
||||||
|
assertTrue(expectedMapping.containsAll(OpenGroupMigrator.getExistingMappings(listOf(legacyThread), listOf(newThread))))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `it should return no mappings if there are no legacy open groups`() {
|
||||||
|
val mappings = OpenGroupMigrator.getExistingMappings(listOf(), listOf())
|
||||||
|
assertTrue(mappings.isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `it should return no mappings if there are only new open groups`() {
|
||||||
|
val newThread = newThreadRecord()
|
||||||
|
val mappings = OpenGroupMigrator.getExistingMappings(emptyList(), listOf(newThread))
|
||||||
|
assertTrue(mappings.isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `it should return null new thread in mappings if there are only legacy open groups`() {
|
||||||
|
val legacyThread = legacyThreadRecord()
|
||||||
|
val mappings = OpenGroupMigrator.getExistingMappings(listOf(legacyThread), emptyList())
|
||||||
|
val expectedMappings = listOf(
|
||||||
|
OpenGroupMapping(OXEN_STUB_HEX, LEGACY_THREAD_ID, null)
|
||||||
|
)
|
||||||
|
assertTrue(expectedMappings.containsAll(mappings))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test migration thread DB calls legacy and returns if no legacy official groups`() {
|
||||||
|
val mockedThreadDb = mock<ThreadDatabase> {
|
||||||
|
on { legacyOxenOpenGroups } doReturn emptyList()
|
||||||
|
}
|
||||||
|
val mockedDbComponent = mock<DatabaseComponent> {
|
||||||
|
on { threadDatabase() } doReturn mockedThreadDb
|
||||||
|
}
|
||||||
|
|
||||||
|
OpenGroupMigrator.migrate(mockedDbComponent)
|
||||||
|
|
||||||
|
verify(mockedDbComponent).threadDatabase()
|
||||||
|
verify(mockedThreadDb).legacyOxenOpenGroups
|
||||||
|
verifyNoMoreInteractions(mockedThreadDb)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `it should migrate on thread, group and loki dbs with correct values for legacy only migration`() {
|
||||||
|
// mock threadDB
|
||||||
|
val capturedThreadId = argumentCaptor<Long>()
|
||||||
|
val capturedNewEncoded = argumentCaptor<String>()
|
||||||
|
val mockedThreadDb = mock<ThreadDatabase> {
|
||||||
|
val legacyThreadRecord = legacyThreadRecord()
|
||||||
|
on { legacyOxenOpenGroups } doReturn listOf(legacyThreadRecord)
|
||||||
|
on { httpsOxenOpenGroups } doReturn emptyList()
|
||||||
|
on { migrateEncodedGroup(capturedThreadId.capture(), capturedNewEncoded.capture()) } doAnswer {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// mock groupDB
|
||||||
|
val capturedGroupLegacyEncoded = argumentCaptor<String>()
|
||||||
|
val capturedGroupNewEncoded = argumentCaptor<String>()
|
||||||
|
val mockedGroupDb = mock<GroupDatabase> {
|
||||||
|
on {
|
||||||
|
migrateEncodedGroup(
|
||||||
|
capturedGroupLegacyEncoded.capture(),
|
||||||
|
capturedGroupNewEncoded.capture()
|
||||||
|
)
|
||||||
|
} doAnswer {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// mock LokiAPIDB
|
||||||
|
val capturedLokiLegacyGroup = argumentCaptor<String>()
|
||||||
|
val capturedLokiNewGroup = argumentCaptor<String>()
|
||||||
|
val mockedLokiApi = mock<LokiAPIDatabase> {
|
||||||
|
on { migrateLegacyOpenGroup(capturedLokiLegacyGroup.capture(), capturedLokiNewGroup.capture()) } doAnswer {}
|
||||||
|
}
|
||||||
|
|
||||||
|
val pubKey = OpenGroupApi.defaultServerPublicKey
|
||||||
|
val room = "oxen"
|
||||||
|
val legacyServer = OpenGroupApi.legacyDefaultServer
|
||||||
|
val newServer = OpenGroupApi.defaultServer
|
||||||
|
|
||||||
|
val lokiThreadOpenGroup = argumentCaptor<OpenGroup>()
|
||||||
|
val mockedLokiThreadDb = mock<LokiThreadDatabase> {
|
||||||
|
on { getOpenGroupChat(eq(LEGACY_THREAD_ID)) } doReturn OpenGroup(legacyServer, room, "Oxen", 0, pubKey)
|
||||||
|
on { setOpenGroupChat(lokiThreadOpenGroup.capture(), eq(LEGACY_THREAD_ID)) } doAnswer {}
|
||||||
|
}
|
||||||
|
|
||||||
|
val mockedDbComponent = mock<DatabaseComponent> {
|
||||||
|
on { threadDatabase() } doReturn mockedThreadDb
|
||||||
|
on { groupDatabase() } doReturn mockedGroupDb
|
||||||
|
on { lokiAPIDatabase() } doReturn mockedLokiApi
|
||||||
|
on { lokiThreadDatabase() } doReturn mockedLokiThreadDb
|
||||||
|
}
|
||||||
|
|
||||||
|
OpenGroupMigrator.migrate(mockedDbComponent)
|
||||||
|
|
||||||
|
// expect threadDB migration to reflect new thread values:
|
||||||
|
// thread ID = 1, encoded ID = new encoded ID
|
||||||
|
assertEquals(LEGACY_THREAD_ID, capturedThreadId.firstValue)
|
||||||
|
assertEquals(EXAMPLE_NEW_ENCODED_OPEN_GROUP, capturedNewEncoded.firstValue)
|
||||||
|
|
||||||
|
// expect groupDB migration to reflect new thread values:
|
||||||
|
// legacy encoded ID, new encoded ID
|
||||||
|
assertEquals(EXAMPLE_LEGACY_ENCODED_OPEN_GROUP, capturedGroupLegacyEncoded.firstValue)
|
||||||
|
assertEquals(EXAMPLE_NEW_ENCODED_OPEN_GROUP, capturedGroupNewEncoded.firstValue)
|
||||||
|
|
||||||
|
// expect Loki API DB migration to reflect new thread values:
|
||||||
|
assertEquals("${OpenGroupApi.legacyDefaultServer}.oxen", capturedLokiLegacyGroup.firstValue)
|
||||||
|
assertEquals("${OpenGroupApi.defaultServer}.oxen", capturedLokiNewGroup.firstValue)
|
||||||
|
|
||||||
|
assertEquals(newServer, lokiThreadOpenGroup.firstValue.server)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `it should migrate and delete legacy thread with conflicting new and old values`() {
|
||||||
|
|
||||||
|
// mock threadDB
|
||||||
|
val capturedThreadId = argumentCaptor<Long>()
|
||||||
|
val mockedThreadDb = mock<ThreadDatabase> {
|
||||||
|
val legacyThreadRecord = legacyThreadRecord()
|
||||||
|
val newThreadRecord = newThreadRecord()
|
||||||
|
on { legacyOxenOpenGroups } doReturn listOf(legacyThreadRecord)
|
||||||
|
on { httpsOxenOpenGroups } doReturn listOf(newThreadRecord)
|
||||||
|
on { deleteConversation(capturedThreadId.capture()) } doAnswer {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// mock groupDB
|
||||||
|
val capturedGroupLegacyEncoded = argumentCaptor<String>()
|
||||||
|
val mockedGroupDb = mock<GroupDatabase> {
|
||||||
|
on { delete(capturedGroupLegacyEncoded.capture()) } doReturn true
|
||||||
|
}
|
||||||
|
|
||||||
|
// mock LokiAPIDB
|
||||||
|
val capturedLokiLegacyGroup = argumentCaptor<String>()
|
||||||
|
val capturedLokiNewGroup = argumentCaptor<String>()
|
||||||
|
val mockedLokiApi = mock<LokiAPIDatabase> {
|
||||||
|
on { migrateLegacyOpenGroup(capturedLokiLegacyGroup.capture(), capturedLokiNewGroup.capture()) } doAnswer {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// mock messaging dbs
|
||||||
|
val migrateMmsFromThreadId = argumentCaptor<Long>()
|
||||||
|
val migrateMmsToThreadId = argumentCaptor<Long>()
|
||||||
|
|
||||||
|
val mockedMmsDb = mock<MmsDatabase> {
|
||||||
|
on { migrateThreadId(migrateMmsFromThreadId.capture(), migrateMmsToThreadId.capture()) } doAnswer {}
|
||||||
|
}
|
||||||
|
|
||||||
|
val migrateSmsFromThreadId = argumentCaptor<Long>()
|
||||||
|
val migrateSmsToThreadId = argumentCaptor<Long>()
|
||||||
|
val mockedSmsDb = mock<SmsDatabase> {
|
||||||
|
on { migrateThreadId(migrateSmsFromThreadId.capture(), migrateSmsToThreadId.capture()) } doAnswer {}
|
||||||
|
}
|
||||||
|
|
||||||
|
val lokiFromThreadId = argumentCaptor<Long>()
|
||||||
|
val lokiToThreadId = argumentCaptor<Long>()
|
||||||
|
val mockedLokiMessageDatabase = mock<LokiMessageDatabase> {
|
||||||
|
on { migrateThreadId(lokiFromThreadId.capture(), lokiToThreadId.capture()) } doAnswer {}
|
||||||
|
}
|
||||||
|
|
||||||
|
val mockedLokiThreadDb = mock<LokiThreadDatabase> {
|
||||||
|
on { removeOpenGroupChat(eq(LEGACY_THREAD_ID)) } doAnswer {}
|
||||||
|
}
|
||||||
|
|
||||||
|
val mockedDbComponent = mock<DatabaseComponent> {
|
||||||
|
on { threadDatabase() } doReturn mockedThreadDb
|
||||||
|
on { groupDatabase() } doReturn mockedGroupDb
|
||||||
|
on { lokiAPIDatabase() } doReturn mockedLokiApi
|
||||||
|
on { mmsDatabase() } doReturn mockedMmsDb
|
||||||
|
on { smsDatabase() } doReturn mockedSmsDb
|
||||||
|
on { lokiMessageDatabase() } doReturn mockedLokiMessageDatabase
|
||||||
|
on { lokiThreadDatabase() } doReturn mockedLokiThreadDb
|
||||||
|
}
|
||||||
|
|
||||||
|
OpenGroupMigrator.migrate(mockedDbComponent)
|
||||||
|
|
||||||
|
// should delete thread by thread ID
|
||||||
|
assertEquals(LEGACY_THREAD_ID, capturedThreadId.firstValue)
|
||||||
|
|
||||||
|
// should delete group by legacy encoded ID
|
||||||
|
assertEquals(EXAMPLE_LEGACY_ENCODED_OPEN_GROUP, capturedGroupLegacyEncoded.firstValue)
|
||||||
|
|
||||||
|
// should migrate SMS from legacy thread ID to new thread ID
|
||||||
|
assertEquals(LEGACY_THREAD_ID, migrateSmsFromThreadId.firstValue)
|
||||||
|
assertEquals(NEW_THREAD_ID, migrateSmsToThreadId.firstValue)
|
||||||
|
|
||||||
|
// should migrate MMS from legacy thread ID to new thread ID
|
||||||
|
assertEquals(LEGACY_THREAD_ID, migrateMmsFromThreadId.firstValue)
|
||||||
|
assertEquals(NEW_THREAD_ID, migrateMmsToThreadId.firstValue)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -65,7 +65,7 @@ interface StorageProtocol {
|
|||||||
fun updateOpenGroup(openGroup: OpenGroup)
|
fun updateOpenGroup(openGroup: OpenGroup)
|
||||||
fun getOpenGroup(threadId: Long): OpenGroup?
|
fun getOpenGroup(threadId: Long): OpenGroup?
|
||||||
fun addOpenGroup(urlAsString: String)
|
fun addOpenGroup(urlAsString: String)
|
||||||
fun onOpenGroupAdded(urlAsString: String)
|
fun onOpenGroupAdded(server: String)
|
||||||
fun hasBackgroundGroupAddJob(groupJoinUrl: String): Boolean
|
fun hasBackgroundGroupAddJob(groupJoinUrl: String): Boolean
|
||||||
fun setOpenGroupServerMessageID(messageID: Long, serverID: Long, threadID: Long, isSms: Boolean)
|
fun setOpenGroupServerMessageID(messageID: Long, serverID: Long, threadID: Long, isSms: Boolean)
|
||||||
fun getOpenGroup(room: String, server: String): OpenGroup?
|
fun getOpenGroup(room: String, server: String): OpenGroup?
|
||||||
|
@ -6,6 +6,7 @@ import org.session.libsession.messaging.open_groups.OpenGroup
|
|||||||
import org.session.libsession.messaging.open_groups.OpenGroupApi
|
import org.session.libsession.messaging.open_groups.OpenGroupApi
|
||||||
import org.session.libsession.messaging.utilities.Data
|
import org.session.libsession.messaging.utilities.Data
|
||||||
import org.session.libsession.utilities.GroupUtil
|
import org.session.libsession.utilities.GroupUtil
|
||||||
|
import org.session.libsession.utilities.OpenGroupUrlParser
|
||||||
import org.session.libsignal.utilities.Log
|
import org.session.libsignal.utilities.Log
|
||||||
|
|
||||||
class BackgroundGroupAddJob(val joinUrl: String): Job {
|
class BackgroundGroupAddJob(val joinUrl: String): Job {
|
||||||
@ -30,31 +31,27 @@ class BackgroundGroupAddJob(val joinUrl: String): Job {
|
|||||||
|
|
||||||
override fun execute() {
|
override fun execute() {
|
||||||
try {
|
try {
|
||||||
|
val openGroup = OpenGroupUrlParser.parseUrl(joinUrl)
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
val allOpenGroups = storage.getAllOpenGroups().map { it.value.joinURL }
|
val allOpenGroups = storage.getAllOpenGroups().map { it.value.joinURL }
|
||||||
if (allOpenGroups.contains(joinUrl)) {
|
if (allOpenGroups.contains(openGroup.joinUrl())) {
|
||||||
Log.e("OpenGroupDispatcher", "Failed to add group because", DuplicateGroupException())
|
Log.e("OpenGroupDispatcher", "Failed to add group because", DuplicateGroupException())
|
||||||
delegate?.handleJobFailed(this, DuplicateGroupException())
|
delegate?.handleJobFailed(this, DuplicateGroupException())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// get image
|
// get image
|
||||||
val url = HttpUrl.parse(joinUrl) ?: throw Exception("Group joinUrl isn't valid")
|
storage.setOpenGroupPublicKey(openGroup.server, openGroup.serverPublicKey)
|
||||||
val server = OpenGroup.getServer(joinUrl)
|
val info = OpenGroupApi.getRoomInfo(openGroup.room, openGroup.server).get()
|
||||||
val serverString = server.toString().removeSuffix("/")
|
|
||||||
val publicKey = url.queryParameter("public_key") ?: throw Exception("Group public key isn't valid")
|
|
||||||
val room = url.pathSegments().firstOrNull() ?: throw Exception("Group room isn't valid")
|
|
||||||
storage.setOpenGroupPublicKey(serverString, publicKey)
|
|
||||||
// get info and auth token
|
|
||||||
storage.addOpenGroup(joinUrl)
|
|
||||||
val info = OpenGroupApi.getRoomInfo(room, serverString).get()
|
|
||||||
val imageId = info.imageId
|
val imageId = info.imageId
|
||||||
|
storage.addOpenGroup(openGroup.joinUrl())
|
||||||
if (imageId != null) {
|
if (imageId != null) {
|
||||||
val bytes = OpenGroupApi.downloadOpenGroupProfilePicture(serverString, room, imageId).get()
|
val bytes = OpenGroupApi.downloadOpenGroupProfilePicture(openGroup.server, openGroup.room, imageId).get()
|
||||||
val groupId = GroupUtil.getEncodedOpenGroupID("$server.$room".toByteArray())
|
val groupId = GroupUtil.getEncodedOpenGroupID("${openGroup.server}.${openGroup.room}".toByteArray())
|
||||||
storage.updateProfilePicture(groupId, bytes)
|
storage.updateProfilePicture(groupId, bytes)
|
||||||
storage.updateTimestampUpdated(groupId, System.currentTimeMillis())
|
storage.updateTimestampUpdated(groupId, System.currentTimeMillis())
|
||||||
}
|
}
|
||||||
storage.onOpenGroupAdded(joinUrl)
|
Log.d(KEY, "onOpenGroupAdded(${openGroup.server})")
|
||||||
|
storage.onOpenGroupAdded(openGroup.server)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("OpenGroupDispatcher", "Failed to add group because",e)
|
Log.e("OpenGroupDispatcher", "Failed to add group because",e)
|
||||||
delegate?.handleJobFailed(this, e)
|
delegate?.handleJobFailed(this, e)
|
||||||
|
@ -55,9 +55,14 @@ object OpenGroupApi {
|
|||||||
now - lastOpenDate
|
now - lastOpenDate
|
||||||
}
|
}
|
||||||
|
|
||||||
const val defaultServerPublicKey =
|
const val defaultServerPublicKey = "a03c383cf63c3c4efe67acc52112a6dd734b3a946b9545f488aaa93da7991238"
|
||||||
"a03c383cf63c3c4efe67acc52112a6dd734b3a946b9545f488aaa93da7991238"
|
const val legacyServerIP = "116.203.70.33"
|
||||||
const val defaultServer = "http://116.203.70.33"
|
const val legacyDefaultServer = "http://116.203.70.33" // TODO: migrate all references to use new value
|
||||||
|
|
||||||
|
/** For migration purposes only, don't use this value in joining groups */
|
||||||
|
const val httpDefaultServer = "http://open.getsession.org"
|
||||||
|
|
||||||
|
const val defaultServer = "https://open.getsession.org"
|
||||||
|
|
||||||
sealed class Error(message: String) : Exception(message) {
|
sealed class Error(message: String) : Exception(message) {
|
||||||
object Generic : Error("An error occurred.")
|
object Generic : Error("An error occurred.")
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
package org.session.libsession.messaging.open_groups
|
||||||
|
|
||||||
|
fun String.migrateLegacyServerUrl() = if (contains(OpenGroupApi.legacyServerIP)) {
|
||||||
|
OpenGroupApi.defaultServer
|
||||||
|
} else if (contains(OpenGroupApi.httpDefaultServer)) {
|
||||||
|
OpenGroupApi.defaultServer
|
||||||
|
} else {
|
||||||
|
this
|
||||||
|
}
|
@ -17,6 +17,7 @@ import org.session.libsession.messaging.messages.control.TypingIndicator
|
|||||||
import org.session.libsession.messaging.messages.control.UnsendRequest
|
import org.session.libsession.messaging.messages.control.UnsendRequest
|
||||||
import org.session.libsession.messaging.messages.visible.Attachment
|
import org.session.libsession.messaging.messages.visible.Attachment
|
||||||
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
||||||
|
import org.session.libsession.messaging.open_groups.OpenGroupApi
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.PointerAttachment
|
import org.session.libsession.messaging.sending_receiving.attachments.PointerAttachment
|
||||||
import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage
|
import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage
|
||||||
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
|
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
|
||||||
@ -157,7 +158,10 @@ private fun handleConfigurationMessage(message: ConfigurationMessage) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
val allV2OpenGroups = storage.getAllOpenGroups().map { it.value.joinURL }
|
val allV2OpenGroups = storage.getAllOpenGroups().map { it.value.joinURL }
|
||||||
for (openGroup in message.openGroups) {
|
for (openGroup in message.openGroups.map {
|
||||||
|
it.replace(OpenGroupApi.legacyDefaultServer, OpenGroupApi.defaultServer)
|
||||||
|
.replace(OpenGroupApi.httpDefaultServer, OpenGroupApi.defaultServer)
|
||||||
|
}) {
|
||||||
if (allV2OpenGroups.contains(openGroup)) continue
|
if (allV2OpenGroups.contains(openGroup)) continue
|
||||||
Log.d("OpenGroup", "All open groups doesn't contain $openGroup")
|
Log.d("OpenGroup", "All open groups doesn't contain $openGroup")
|
||||||
if (!storage.hasBackgroundGroupAddJob(openGroup)) {
|
if (!storage.hasBackgroundGroupAddJob(openGroup)) {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package org.session.libsession.utilities
|
package org.session.libsession.utilities
|
||||||
|
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
|
import org.session.libsession.messaging.open_groups.migrateLegacyServerUrl
|
||||||
|
|
||||||
object OpenGroupUrlParser {
|
object OpenGroupUrlParser {
|
||||||
|
|
||||||
@ -20,7 +21,7 @@ object OpenGroupUrlParser {
|
|||||||
// If the URL is malformed, throw an exception
|
// If the URL is malformed, throw an exception
|
||||||
val url = HttpUrl.parse(urlWithPrefix) ?: throw Error.MalformedURL
|
val url = HttpUrl.parse(urlWithPrefix) ?: throw Error.MalformedURL
|
||||||
// Parse components
|
// Parse components
|
||||||
val server = HttpUrl.Builder().scheme(url.scheme()).host(url.host()).port(url.port()).build().toString().removeSuffix(suffix)
|
val server = HttpUrl.Builder().scheme(url.scheme()).host(url.host()).port(url.port()).build().toString().removeSuffix(suffix).migrateLegacyServerUrl()
|
||||||
val room = url.pathSegments().firstOrNull { !it.isNullOrEmpty() } ?: throw Error.NoRoom
|
val room = url.pathSegments().firstOrNull { !it.isNullOrEmpty() } ?: throw Error.NoRoom
|
||||||
val publicKey = url.queryParameter(queryPrefix) ?: throw Error.NoPublicKey
|
val publicKey = url.queryParameter(queryPrefix) ?: throw Error.NoPublicKey
|
||||||
if (publicKey.length != 64) throw Error.InvalidPublicKey
|
if (publicKey.length != 64) throw Error.InvalidPublicKey
|
||||||
@ -33,4 +34,6 @@ object OpenGroupUrlParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class V2OpenGroupInfo(val server: String, val room: String, val serverPublicKey: String)
|
class V2OpenGroupInfo(val server: String, val room: String, val serverPublicKey: String) {
|
||||||
|
fun joinUrl() = "$server/$room?public_key=$serverPublicKey"
|
||||||
|
}
|
@ -20,7 +20,6 @@ interface LokiAPIDatabaseProtocol {
|
|||||||
fun setReceivedMessageHashValues(publicKey: String, newValue: Set<String>, namespace: Int)
|
fun setReceivedMessageHashValues(publicKey: String, newValue: Set<String>, namespace: Int)
|
||||||
fun getAuthToken(server: String): String?
|
fun getAuthToken(server: String): String?
|
||||||
fun setAuthToken(server: String, newValue: String?)
|
fun setAuthToken(server: String, newValue: String?)
|
||||||
fun setUserCount(group: Long, server: String, newValue: Int)
|
|
||||||
fun setUserCount(room: String, server: String, newValue: Int)
|
fun setUserCount(room: String, server: String, newValue: Int)
|
||||||
fun getLastMessageServerID(room: String, server: String): Long?
|
fun getLastMessageServerID(room: String, server: String): Long?
|
||||||
fun setLastMessageServerID(room: String, server: String, newValue: Long)
|
fun setLastMessageServerID(room: String, server: String, newValue: Long)
|
||||||
@ -36,5 +35,5 @@ interface LokiAPIDatabaseProtocol {
|
|||||||
fun isClosedGroup(groupPublicKey: String): Boolean
|
fun isClosedGroup(groupPublicKey: String): Boolean
|
||||||
fun getForkInfo(): ForkInfo
|
fun getForkInfo(): ForkInfo
|
||||||
fun setForkInfo(forkInfo: ForkInfo)
|
fun setForkInfo(forkInfo: ForkInfo)
|
||||||
|
fun migrateLegacyOpenGroup(legacyServerId: String, newServerId: String)
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user