package org.thoughtcrime.securesms.groups import android.content.Context import androidx.annotation.WorkerThread import okhttp3.HttpUrl import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.open_groups.GroupMemberRole import org.session.libsession.messaging.open_groups.OpenGroup import org.session.libsession.messaging.open_groups.OpenGroupApi import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPoller import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities import java.util.concurrent.Executors object OpenGroupManager { private val executorService = Executors.newScheduledThreadPool(4) private val pollers = mutableMapOf() // One for each server private var isPolling = false private val pollUpdaterLock = Any() val isAllCaughtUp: Boolean get() { pollers.values.forEach { poller -> val jobID = poller.secondToLastJob?.id jobID?.let { val storage = MessagingModuleConfiguration.shared.storage if (storage.getMessageReceiveJob(jobID) == null) { // If the second to last job is done, it means we are now handling the last job poller.isCaughtUp = true poller.secondToLastJob = null } } if (!poller.isCaughtUp) { return false } } return true } fun startPolling() { if (isPolling) { return } isPolling = true val storage = MessagingModuleConfiguration.shared.storage val (serverGroups, toDelete) = storage.getAllOpenGroups().values.partition { storage.getThreadId(it) != null } toDelete.forEach { openGroup -> Log.w("Loki", "Need to delete a group") delete(openGroup.server, openGroup.room, MessagingModuleConfiguration.shared.context) } val servers = serverGroups.map { it.server }.toSet() synchronized(pollUpdaterLock) { servers.forEach { server -> pollers[server]?.stop() // Shouldn't be necessary pollers[server] = OpenGroupPoller(server, executorService).apply { startIfNeeded() } } } } fun stopPolling() { synchronized(pollUpdaterLock) { pollers.forEach { it.value.stop() } pollers.clear() isPolling = false } } @WorkerThread fun add(server: String, room: String, publicKey: String, context: Context): Pair { val openGroupID = "$server.$room" val 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 threadID to null } // Clear any existing data if needed storage.removeLastDeletionServerID(room, server) storage.removeLastMessageServerID(room, server) storage.removeLastInboxMessageId(server) storage.removeLastOutboxMessageId(server) // Store the public key storage.setOpenGroupPublicKey(server, publicKey) // Get capabilities & room info val (capabilities, info) = OpenGroupApi.getCapabilitiesAndRoomInfo(room, server).get() storage.setServerCapabilities(server, capabilities.capabilities) // Create the group locally if not available already if (threadID < 0) { GroupManager.createOpenGroup(openGroupID, context, null, info.name) } OpenGroupPoller.handleRoomPollInfo( server = server, roomToken = room, pollInfo = info.toPollInfo(), createGroupIfMissingWithPublicKey = publicKey ) return threadID to info } fun restartPollerForServer(server: String) { // Start the poller if needed synchronized(pollUpdaterLock) { pollers[server]?.stop() pollers[server]?.startIfNeeded() ?: run { val poller = OpenGroupPoller(server, executorService) Log.d("Loki", "Starting poller for open group: $server") pollers[server] = poller poller.startIfNeeded() } } } @WorkerThread fun delete(server: String, room: String, context: Context) { val storage = MessagingModuleConfiguration.shared.storage val configFactory = MessagingModuleConfiguration.shared.configFactory val threadDB = DatabaseComponent.get(context).threadDatabase() val openGroupID = "${server.removeSuffix("/")}.$room" val threadID = GroupManager.getOpenGroupThreadID(openGroupID, context) val recipient = threadDB.getRecipientForThreadId(threadID) ?: return threadDB.setThreadArchived(threadID) val groupID = recipient.address.serialize() // Stop the poller if needed val openGroups = storage.getAllOpenGroups().filter { it.value.server == server } if (openGroups.isNotEmpty()) { synchronized(pollUpdaterLock) { val poller = pollers[server] poller?.stop() pollers.remove(server) } } configFactory.userGroups?.eraseCommunity(server, room) configFactory.convoVolatile?.eraseCommunity(server, room) // Delete storage.removeLastDeletionServerID(room, server) storage.removeLastMessageServerID(room, server) storage.removeLastInboxMessageId(server) storage.removeLastOutboxMessageId(server) val lokiThreadDB = DatabaseComponent.get(context).lokiThreadDatabase() lokiThreadDB.removeOpenGroupChat(threadID) storage.deleteConversation(threadID) // Must be invoked on a background thread GroupManager.deleteGroup(groupID, context) // Must be invoked on a background thread ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context) } @WorkerThread 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 null val publicKey = url.queryParameter("public_key") ?: return null return add(server.toString().removeSuffix("/"), room, publicKey, context).second // assume migrated from calling function } fun updateOpenGroup(openGroup: OpenGroup, context: Context) { val threadDB = DatabaseComponent.get(context).lokiThreadDatabase() val openGroupID = "${openGroup.server}.${openGroup.room}" val threadID = GroupManager.getOpenGroupThreadID(openGroupID, context) threadDB.setOpenGroupChat(openGroup, threadID) } fun isUserModerator(context: Context, groupId: String, standardPublicKey: String, blindedPublicKey: String? = null): Boolean { val memberDatabase = DatabaseComponent.get(context).groupMemberDatabase() val standardRoles = memberDatabase.getGroupMemberRoles(groupId, standardPublicKey) val blindedRoles = blindedPublicKey?.let { memberDatabase.getGroupMemberRoles(groupId, it) } ?: emptyList() // roles to check against val moderatorRoles = listOf( GroupMemberRole.MODERATOR, GroupMemberRole.ADMIN, GroupMemberRole.HIDDEN_MODERATOR, GroupMemberRole.HIDDEN_ADMIN ) return standardRoles.any { it in moderatorRoles } || blindedRoles.any { it in moderatorRoles } } }