TelegramBotAPI-examples/RightsChangerBot/src/main/kotlin/RightsChanger.kt

541 lines
22 KiB
Kotlin
Raw Normal View History

2024-02-16 19:51:32 +00:00
import dev.inmo.kslog.common.KSLog
import dev.inmo.kslog.common.LogLevel
import dev.inmo.kslog.common.defaultMessageFormatter
import dev.inmo.kslog.common.setDefaultKSLog
2023-09-24 18:19:34 +00:00
import dev.inmo.micro_utils.coroutines.firstOf
2024-02-16 19:51:32 +00:00
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
2023-09-24 18:19:34 +00:00
import dev.inmo.micro_utils.fsm.common.State
2023-02-06 06:08:25 +00:00
import dev.inmo.tgbotapi.bot.ktor.telegramBot
import dev.inmo.tgbotapi.extensions.api.bot.setMyCommands
import dev.inmo.tgbotapi.extensions.api.chat.get.getChat
import dev.inmo.tgbotapi.extensions.api.chat.members.getChatMember
2023-09-24 18:19:34 +00:00
import dev.inmo.tgbotapi.extensions.api.chat.members.promoteChannelAdministrator
2023-02-06 06:08:25 +00:00
import dev.inmo.tgbotapi.extensions.api.chat.members.restrictChatMember
import dev.inmo.tgbotapi.extensions.api.edit.edit
import dev.inmo.tgbotapi.extensions.api.send.reply
2023-09-24 18:19:34 +00:00
import dev.inmo.tgbotapi.extensions.api.send.send
2023-02-06 06:08:25 +00:00
import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContext
2023-09-24 18:19:34 +00:00
import dev.inmo.tgbotapi.extensions.behaviour_builder.buildBehaviourWithFSMAndStartLongPolling
import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.*
2023-02-06 06:08:25 +00:00
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onCommand
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onMessageDataCallbackQuery
2023-09-24 18:19:34 +00:00
import dev.inmo.tgbotapi.extensions.utils.*
import dev.inmo.tgbotapi.extensions.utils.extensions.sameChat
import dev.inmo.tgbotapi.extensions.utils.types.buttons.*
import dev.inmo.tgbotapi.types.*
2023-02-06 06:08:25 +00:00
import dev.inmo.tgbotapi.types.buttons.InlineKeyboardMarkup
2023-09-24 18:19:34 +00:00
import dev.inmo.tgbotapi.types.chat.ChannelChat
2023-02-06 06:08:25 +00:00
import dev.inmo.tgbotapi.types.chat.ChatPermissions
import dev.inmo.tgbotapi.types.chat.PublicChat
2023-09-24 18:19:34 +00:00
import dev.inmo.tgbotapi.types.chat.member.*
2023-02-06 06:08:25 +00:00
import dev.inmo.tgbotapi.types.commands.BotCommandScope
import dev.inmo.tgbotapi.types.message.abstracts.AccessibleMessage
2023-09-24 18:19:34 +00:00
import dev.inmo.tgbotapi.types.request.RequestId
2023-09-25 08:55:37 +00:00
import dev.inmo.tgbotapi.utils.*
2023-09-24 18:19:34 +00:00
import dev.inmo.tgbotapi.utils.mention
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.mapNotNull
sealed interface UserRetrievingStep : State {
data class RetrievingChannelChatState(
override val context: ChatId
) : UserRetrievingStep
data class RetrievingUserIdChatState(
override val context: ChatId,
val channelId: ChatId
) : UserRetrievingStep
data class RetrievingChatInfoDoneState(
override val context: ChatId,
val channelId: ChatId,
val userId: UserId
) : UserRetrievingStep
}
2023-02-06 06:08:25 +00:00
suspend fun main(args: Array<String>) {
val botToken = args.first()
2024-02-16 19:51:32 +00:00
val isDebug = args.getOrNull(2) == "debug"
if (isDebug) {
setDefaultKSLog(
KSLog { level: LogLevel, tag: String?, message: Any, throwable: Throwable? ->
println(defaultMessageFormatter(level, tag, message, throwable))
}
)
}
2023-02-06 06:08:25 +00:00
val bot = telegramBot(botToken)
val allowedAdmin = ChatId(args[1].toLong())
fun Boolean?.allowedSymbol() = when (this) {
true -> ""
false -> ""
null -> ""
}
val granularDataPrefix = "granular"
val messagesToggleGranularData = "$granularDataPrefix messages"
val otherMessagesToggleGranularData = "$granularDataPrefix other messages"
val audiosToggleGranularData = "$granularDataPrefix audios"
val voicesToggleGranularData = "$granularDataPrefix voices"
val videosToggleGranularData = "$granularDataPrefix videos"
val videoNotesToggleGranularData = "$granularDataPrefix video notes"
val photosToggleGranularData = "$granularDataPrefix photos"
val webPagePreviewToggleGranularData = "$granularDataPrefix web page preview"
val pollsToggleGranularData = "$granularDataPrefix polls"
val documentsToggleGranularData = "$granularDataPrefix documents"
val commonDataPrefix = "common"
val pollsToggleCommonData = "$commonDataPrefix polls"
val otherMessagesToggleCommonData = "$commonDataPrefix other messages"
val webPagePreviewToggleCommonData = "$commonDataPrefix web page preview"
2023-09-24 18:19:34 +00:00
val adminRightsDataPrefix = "admin"
2023-09-25 08:55:37 +00:00
val refreshAdminRightsData = "${adminRightsDataPrefix}_refresh"
2023-09-24 18:19:34 +00:00
val postMessagesToggleAdminRightsData = "${adminRightsDataPrefix}_post_messages"
val editMessagesToggleAdminRightsData = "${adminRightsDataPrefix}_edit_messages"
val deleteMessagesToggleAdminRightsData = "${adminRightsDataPrefix}_delete_messages"
val editStoriesToggleAdminRightsData = "${adminRightsDataPrefix}_edit_stories"
val deleteStoriesToggleAdminRightsData = "${adminRightsDataPrefix}_delete_stories"
val postStoriesToggleAdminRightsData = "${adminRightsDataPrefix}_post_stories"
2023-02-06 06:08:25 +00:00
suspend fun BehaviourContext.getUserChatPermissions(chatId: ChatId, userId: UserId): ChatPermissions? {
val chatMember = getChatMember(chatId, userId)
return chatMember.restrictedChatMemberOrNull() ?: chatMember.whenMemberChatMember {
getChat(chatId).extendedGroupChatOrNull() ?.permissions
}
}
2023-09-24 18:19:34 +00:00
fun buildGranularKeyboard(
permissions: ChatPermissions
): InlineKeyboardMarkup {
2023-02-06 06:08:25 +00:00
return inlineKeyboard {
row {
dataButton("Send messages${permissions.canSendMessages.allowedSymbol()}", messagesToggleGranularData)
2023-09-24 18:19:34 +00:00
dataButton(
"Send other messages${permissions.canSendOtherMessages.allowedSymbol()}",
otherMessagesToggleGranularData
)
2023-02-06 06:08:25 +00:00
}
row {
dataButton("Send audios${permissions.canSendAudios.allowedSymbol()}", audiosToggleGranularData)
dataButton("Send voices${permissions.canSendVoiceNotes.allowedSymbol()}", voicesToggleGranularData)
}
row {
dataButton("Send videos${permissions.canSendVideos.allowedSymbol()}", videosToggleGranularData)
2023-09-24 18:19:34 +00:00
dataButton(
"Send video notes${permissions.canSendVideoNotes.allowedSymbol()}",
videoNotesToggleGranularData
)
2023-02-06 06:08:25 +00:00
}
row {
dataButton("Send photos${permissions.canSendPhotos.allowedSymbol()}", photosToggleGranularData)
2023-09-24 18:19:34 +00:00
dataButton(
"Add web preview${permissions.canAddWebPagePreviews.allowedSymbol()}",
webPagePreviewToggleGranularData
)
2023-02-06 06:08:25 +00:00
}
row {
dataButton("Send polls${permissions.canSendPolls.allowedSymbol()}", pollsToggleGranularData)
dataButton("Send documents${permissions.canSendDocuments.allowedSymbol()}", documentsToggleGranularData)
}
}
}
2023-09-24 18:19:34 +00:00
fun buildAdminRightsKeyboard(
2023-09-25 08:55:37 +00:00
permissions: AdministratorChatMember?,
2023-09-24 18:19:34 +00:00
channelId: ChatId,
userId: UserId
): InlineKeyboardMarkup {
return inlineKeyboard {
2023-09-25 08:55:37 +00:00
permissions ?.also {
row {
dataButton("Refresh", "$refreshAdminRightsData ${channelId.chatId} ${userId.chatId}")
}
row {
dataButton("Edit messages${permissions.canEditMessages.allowedSymbol()}", "$editMessagesToggleAdminRightsData ${channelId.chatId} ${userId.chatId}")
dataButton("Delete messages${permissions.canRemoveMessages.allowedSymbol()}", "$deleteMessagesToggleAdminRightsData ${channelId.chatId} ${userId.chatId}")
}
row {
dataButton("Post messages${permissions.canPostMessages.allowedSymbol()}", "$postMessagesToggleAdminRightsData ${channelId.chatId} ${userId.chatId}")
}
row {
dataButton("Edit stories${permissions.canEditStories.allowedSymbol()}", "$editStoriesToggleAdminRightsData ${channelId.chatId} ${userId.chatId}")
dataButton("Delete stories${permissions.canDeleteStories.allowedSymbol()}", "$deleteStoriesToggleAdminRightsData ${channelId.chatId} ${userId.chatId}")
}
row {
dataButton("Post stories${permissions.canPostStories.allowedSymbol()}", "$postStoriesToggleAdminRightsData ${channelId.chatId} ${userId.chatId}")
}
} ?: row {
dataButton("Promote to admin", "$postMessagesToggleAdminRightsData ${channelId.chatId} ${userId.chatId}")
2023-09-24 18:19:34 +00:00
}
}
}
suspend fun BehaviourContext.buildGranularKeyboard(chatId: ChatId, userId: UserId): InlineKeyboardMarkup? {
return buildGranularKeyboard(
getUserChatPermissions(chatId, userId) ?: return null
)
}
2023-02-06 06:08:25 +00:00
suspend fun BehaviourContext.buildCommonKeyboard(chatId: ChatId, userId: UserId): InlineKeyboardMarkup? {
val permissions = getUserChatPermissions(chatId, userId) ?: return null
return inlineKeyboard {
row {
dataButton("Send polls${permissions.canSendPolls.allowedSymbol()}", pollsToggleCommonData)
}
row {
dataButton("Send other messages${permissions.canSendOtherMessages.allowedSymbol()}", otherMessagesToggleCommonData)
}
row {
dataButton("Add web preview${permissions.canAddWebPagePreviews.allowedSymbol()}", webPagePreviewToggleCommonData)
}
}
}
2023-09-24 18:19:34 +00:00
bot.buildBehaviourWithFSMAndStartLongPolling<UserRetrievingStep>(
2023-02-27 16:38:05 +00:00
defaultExceptionsHandler = {
2023-06-30 21:54:42 +00:00
it.printStackTrace()
2023-09-24 18:19:34 +00:00
},
2023-02-27 16:38:05 +00:00
) {
2023-09-24 18:19:34 +00:00
onCommand(
"simple",
initialFilter = { it.chat is PublicChat && it.fromUserMessageOrNull()?.user?.id == allowedAdmin }
) {
2023-02-06 06:08:25 +00:00
val replyMessage = it.replyTo
2023-09-24 18:19:34 +00:00
val userInReply = replyMessage?.fromUserMessageOrNull()?.user?.id ?: return@onCommand
if (replyMessage is AccessibleMessage) {
reply(
replyMessage,
"Manage keyboard:",
replyMarkup = buildCommonKeyboard(it.chat.id.toChatId(), userInReply) ?: return@onCommand
)
} else {
reply(it) {
regular("Reply to somebody's message to get hist/her rights keyboard")
}
}
2023-02-06 06:08:25 +00:00
}
2023-09-24 18:19:34 +00:00
onCommand(
"granular",
initialFilter = {
it.chat is ChannelChat || (it.chat is PublicChat && it.fromUserMessageOrNull()?.user?.id == allowedAdmin)
}
) {
2023-02-06 06:08:25 +00:00
val replyMessage = it.replyTo
2023-09-24 18:19:34 +00:00
val usernameInText = it.content.textSources.firstNotNullOfOrNull { it.mentionTextSourceOrNull() } ?.username
val userInReply = replyMessage?.fromUserMessageOrNull()?.user?.id ?: return@onCommand
if (replyMessage is AccessibleMessage) {
reply(
replyMessage,
"Manage keyboard:",
replyMarkup = buildGranularKeyboard(it.chat.id.toChatId(), userInReply) ?: return@onCommand
)
} else {
reply(it) {
regular("Reply to somebody's message to get hist/her rights keyboard")
}
}
2023-02-06 06:08:25 +00:00
}
onMessageDataCallbackQuery(
Regex("^${granularDataPrefix}.*"),
initialFilter = { it.user.id == allowedAdmin }
) {
2023-09-24 18:19:34 +00:00
val messageReply =
it.message.commonMessageOrNull()?.replyTo?.fromUserMessageOrNull() ?: return@onMessageDataCallbackQuery
2023-02-06 06:08:25 +00:00
val userId = messageReply.user.id
2023-09-24 18:19:34 +00:00
val permissions =
getUserChatPermissions(it.message.chat.id.toChatId(), userId) ?: return@onMessageDataCallbackQuery
2023-02-06 06:08:25 +00:00
val newPermission = when (it.data) {
messagesToggleGranularData -> {
permissions.copyGranular(
2023-09-24 18:19:34 +00:00
canSendMessages = permissions.canSendMessages?.let { !it } ?: false
2023-02-06 06:08:25 +00:00
)
}
2023-09-24 18:19:34 +00:00
2023-02-06 06:08:25 +00:00
otherMessagesToggleGranularData -> {
permissions.copyGranular(
2023-09-24 18:19:34 +00:00
canSendOtherMessages = permissions.canSendOtherMessages?.let { !it } ?: false
2023-02-06 06:08:25 +00:00
)
}
2023-09-24 18:19:34 +00:00
2023-02-06 06:08:25 +00:00
audiosToggleGranularData -> {
permissions.copyGranular(
2023-09-24 18:19:34 +00:00
canSendAudios = permissions.canSendAudios?.let { !it } ?: false
2023-02-06 06:08:25 +00:00
)
}
2023-09-24 18:19:34 +00:00
2023-02-06 06:08:25 +00:00
voicesToggleGranularData -> {
permissions.copyGranular(
2023-09-24 18:19:34 +00:00
canSendVoiceNotes = permissions.canSendVoiceNotes?.let { !it } ?: false
2023-02-06 06:08:25 +00:00
)
}
2023-09-24 18:19:34 +00:00
2023-02-06 06:08:25 +00:00
videosToggleGranularData -> {
permissions.copyGranular(
2023-09-24 18:19:34 +00:00
canSendVideos = permissions.canSendVideos?.let { !it } ?: false
2023-02-06 06:08:25 +00:00
)
}
2023-09-24 18:19:34 +00:00
2023-02-06 06:08:25 +00:00
videoNotesToggleGranularData -> {
permissions.copyGranular(
2023-09-24 18:19:34 +00:00
canSendVideoNotes = permissions.canSendVideoNotes?.let { !it } ?: false
2023-02-06 06:08:25 +00:00
)
}
2023-09-24 18:19:34 +00:00
2023-02-06 06:08:25 +00:00
photosToggleGranularData -> {
permissions.copyGranular(
2023-09-24 18:19:34 +00:00
canSendPhotos = permissions.canSendPhotos?.let { !it } ?: false
2023-02-06 06:08:25 +00:00
)
}
2023-09-24 18:19:34 +00:00
2023-02-06 06:08:25 +00:00
webPagePreviewToggleGranularData -> {
permissions.copyGranular(
2023-09-24 18:19:34 +00:00
canAddWebPagePreviews = permissions.canAddWebPagePreviews?.let { !it } ?: false
2023-02-06 06:08:25 +00:00
)
}
2023-09-24 18:19:34 +00:00
2023-02-06 06:08:25 +00:00
pollsToggleGranularData -> {
permissions.copyGranular(
2023-09-24 18:19:34 +00:00
canSendPolls = permissions.canSendPolls?.let { !it } ?: false
2023-02-06 06:08:25 +00:00
)
}
2023-09-24 18:19:34 +00:00
2023-02-06 06:08:25 +00:00
documentsToggleGranularData -> {
permissions.copyGranular(
2023-09-24 18:19:34 +00:00
canSendDocuments = permissions.canSendDocuments?.let { !it } ?: false
2023-02-06 06:08:25 +00:00
)
}
2023-09-24 18:19:34 +00:00
2023-02-06 06:08:25 +00:00
else -> permissions.copyGranular()
}
restrictChatMember(
it.message.chat.id,
userId,
permissions = newPermission,
useIndependentChatPermissions = true
)
edit(
it.message,
2023-09-24 18:19:34 +00:00
replyMarkup = buildGranularKeyboard(it.message.chat.id.toChatId(), userId)
?: return@onMessageDataCallbackQuery
2023-02-06 06:08:25 +00:00
)
}
onMessageDataCallbackQuery(
Regex("^${commonDataPrefix}.*"),
initialFilter = { it.user.id == allowedAdmin }
) {
2023-09-24 18:19:34 +00:00
val messageReply =
it.message.commonMessageOrNull()?.replyTo?.fromUserMessageOrNull() ?: return@onMessageDataCallbackQuery
2023-02-06 06:08:25 +00:00
val userId = messageReply.user.id
2023-09-24 18:19:34 +00:00
val permissions =
getUserChatPermissions(it.message.chat.id.toChatId(), userId) ?: return@onMessageDataCallbackQuery
2023-02-06 06:08:25 +00:00
val newPermission = when (it.data) {
pollsToggleCommonData -> {
permissions.copyCommon(
2023-09-24 18:19:34 +00:00
canSendPolls = permissions.canSendPolls?.let { !it } ?: false
2023-02-06 06:08:25 +00:00
)
}
2023-09-24 18:19:34 +00:00
2023-02-06 06:08:25 +00:00
otherMessagesToggleCommonData -> {
permissions.copyCommon(
2023-09-24 18:19:34 +00:00
canSendOtherMessages = permissions.canSendOtherMessages?.let { !it } ?: false
2023-02-06 06:08:25 +00:00
)
}
2023-09-24 18:19:34 +00:00
2023-02-06 06:08:25 +00:00
webPagePreviewToggleCommonData -> {
permissions.copyCommon(
2023-09-24 18:19:34 +00:00
canAddWebPagePreviews = permissions.canAddWebPagePreviews?.let { !it } ?: false
2023-02-06 06:08:25 +00:00
)
}
2023-09-24 18:19:34 +00:00
2023-02-06 06:08:25 +00:00
else -> permissions.copyCommon()
}
restrictChatMember(
it.message.chat.id,
userId,
permissions = newPermission,
useIndependentChatPermissions = false
)
edit(
it.message,
2023-09-24 18:19:34 +00:00
replyMarkup = buildCommonKeyboard(it.message.chat.id.toChatId(), userId)
?: return@onMessageDataCallbackQuery
2023-02-06 06:08:25 +00:00
)
}
2023-09-24 18:19:34 +00:00
onMessageDataCallbackQuery(
Regex("^${adminRightsDataPrefix}.*"),
initialFilter = { it.user.id == allowedAdmin }
) {
val (channelIdString, userIdString) = it.data.split(" ").drop(1)
val channelId = ChatId(channelIdString.toLong())
val userId = ChatId(userIdString.toLong())
2023-09-25 08:55:37 +00:00
val chatMember = getChatMember(channelId, userId)
val asAdmin = chatMember.administratorChatMemberOrNull()
val asMember = chatMember.memberChatMemberOrNull()
2023-09-24 18:19:34 +00:00
val realData = it.data.takeWhile { it != ' ' }
2023-09-25 08:55:37 +00:00
fun Boolean?.toggleIfData(data: String) = if (realData == data) {
!(this ?: false)
2023-09-24 18:19:34 +00:00
} else {
null
}
2023-09-25 08:55:37 +00:00
if (realData != refreshAdminRightsData) {
promoteChannelAdministrator(
channelId,
userId,
canPostMessages = asAdmin ?.canPostMessages.toggleIfData(postMessagesToggleAdminRightsData),
canEditMessages = asAdmin ?.canEditMessages.toggleIfData(editMessagesToggleAdminRightsData),
canDeleteMessages = asAdmin ?.canRemoveMessages.toggleIfData(deleteMessagesToggleAdminRightsData),
canEditStories = asAdmin ?.canEditStories.toggleIfData(editStoriesToggleAdminRightsData),
canDeleteStories = asAdmin ?.canDeleteStories.toggleIfData(deleteStoriesToggleAdminRightsData),
canPostStories = asAdmin ?.canPostStories.toggleIfData(postStoriesToggleAdminRightsData),
)
}
2023-09-24 18:19:34 +00:00
edit(
it.message,
replyMarkup = buildAdminRightsKeyboard(
getChatMember(
channelId,
userId
2023-09-25 08:55:37 +00:00
).administratorChatMemberOrNull(),
2023-09-24 18:19:34 +00:00
channelId,
userId
)
)
}
strictlyOn<UserRetrievingStep.RetrievingChannelChatState> { state ->
val requestId = RequestId.random()
send(
state.context,
replyMarkup = replyKeyboard(
oneTimeKeyboard = true,
resizeKeyboard = true
) {
row {
requestChatButton(
"Choose channel",
requestId = requestId,
isChannel = true,
botIsMember = true,
botRightsInChat = ChatCommonAdministratorRights(
canPromoteMembers = true,
canRestrictMembers = true
),
userRightsInChat = ChatCommonAdministratorRights(
canPromoteMembers = true,
canRestrictMembers = true
)
)
}
}
) {
regular("Ok, send me the channel in which you wish to manage user, or use ")
botCommand("cancel")
regular(" to cancel the request")
}
firstOf {
include {
val chatId = waitChatSharedEventsMessages().mapNotNull {
it.chatEvent.chatId.takeIf { _ ->
it.chatEvent.requestId == requestId && it.sameChat(state.context)
}
}.first()
UserRetrievingStep.RetrievingUserIdChatState(state.context, chatId)
}
include {
waitCommandMessage("cancel").filter { it.sameChat(state.context) }.first()
null
}
}
}
strictlyOn<UserRetrievingStep.RetrievingUserIdChatState> { state ->
val requestId = RequestId.random()
send(
state.context,
replyMarkup = replyKeyboard(
oneTimeKeyboard = true,
resizeKeyboard = true
) {
row {
requestUserButton(
"Choose user",
requestId = requestId
)
}
}
) {
regular("Ok, send me the user for which you wish to change rights, or use ")
botCommand("cancel")
regular(" to cancel the request")
}
firstOf {
include {
val userContactChatId = waitUserSharedEventsMessages().filter {
it.sameChat(state.context)
}.first().chatEvent.chatId
UserRetrievingStep.RetrievingChatInfoDoneState(
state.context,
state.channelId,
userContactChatId
)
}
include {
waitCommandMessage("cancel").filter { it.sameChat(state.context) }.first()
null
}
}
}
strictlyOn<UserRetrievingStep.RetrievingChatInfoDoneState> { state ->
val chatMember = getChatMember(state.channelId, state.userId).administratorChatMemberOrNull()
if (chatMember == null) {
return@strictlyOn null
}
send(
state.context,
replyMarkup = buildAdminRightsKeyboard(
chatMember,
state.channelId,
state.userId
)
) {
regular("Rights of ")
2023-09-25 08:55:37 +00:00
mentionln(chatMember.user)
regular("Please, remember, that to be able to change user rights bot must promote user by itself to admin")
2023-09-24 18:19:34 +00:00
}
null
}
onCommand("rights_in_channel") {
startChain(UserRetrievingStep.RetrievingChannelChatState(it.chat.id.toChatId()))
}
2023-02-06 06:08:25 +00:00
setMyCommands(
BotCommand("simple", "Trigger simple keyboard. Use with reply to user"),
BotCommand("granular", "Trigger granular keyboard. Use with reply to user"),
2023-09-24 18:19:34 +00:00
BotCommand("rights_in_channel", "Trigger granular keyboard. Use with reply to user"),
2023-02-06 06:08:25 +00:00
scope = BotCommandScope.AllGroupChats
)
2024-02-16 19:51:32 +00:00
allUpdatesFlow.subscribeSafelyWithoutExceptions(this) {
println(it)
}
2023-02-06 06:08:25 +00:00
}.join()
}