diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f7b361ad8..0b8c7e45aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # TelegramBotAPI changelog +## 5.1.0 + +[Bot API 6.5](https://core.telegram.org/bots/api-changelog#february-3-2023) support + +* `Core`: + * `ChatPermissions` now is interface and have two main realizations: `ChatPermissions.Granular` and + `ChatPermissions.Common` + * `RestrictedChatMember` now implements `ChatPermissions` too +* `API`: + * Now it is possible to pass all long polling parameters in all places used it +* `Issues`: + * Fix of [#697](https://github.com/InsanusMokrassar/TelegramBotAPI/issues/697) + ## 5.0.2 * `Versions`: diff --git a/README.md b/README.md index 728bb966d7..5541d54588 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# TelegramBotAPI [![Maven Central](https://maven-badges.herokuapp.com/maven-central/dev.inmo/tgbotapi/badge.svg)](https://maven-badges.herokuapp.com/maven-central/dev.inmo/tgbotapi) [![Supported version](https://img.shields.io/badge/Telegram%20Bot%20API-6.4-blue)](https://core.telegram.org/bots/api-changelog#december-30-2022) +# TelegramBotAPI [![Maven Central](https://maven-badges.herokuapp.com/maven-central/dev.inmo/tgbotapi/badge.svg)](https://maven-badges.herokuapp.com/maven-central/dev.inmo/tgbotapi) [![Supported version](https://img.shields.io/badge/Telegram%20Bot%20API-6.5-blue)](https://core.telegram.org/bots/api-changelog#february-3-2023) | Docs | [![KDocs](https://img.shields.io/static/v1?label=Dokka&message=KDocs&color=blue&logo=kotlin)](https://tgbotapi.inmo.dev/index.html) [![Mini tutorial](https://img.shields.io/static/v1?label=Bookstack&message=Tutorial&color=blue&logo=bookstack)](https://bookstack.inmo.dev/books/telegrambotapi/chapter/introduction-tutorial) | |:---:|:---:| diff --git a/gradle.properties b/gradle.properties index d5f1dc3067..5290e27fd1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,4 +6,4 @@ kotlin.incremental=true kotlin.incremental.js=true library_group=dev.inmo -library_version=5.0.2 +library_version=5.1.0 diff --git a/tgbotapi.api/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/api/chat/members/RestrictChatMember.kt b/tgbotapi.api/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/api/chat/members/RestrictChatMember.kt index 10ef93a219..2ebf2e47b5 100644 --- a/tgbotapi.api/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/api/chat/members/RestrictChatMember.kt +++ b/tgbotapi.api/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/api/chat/members/RestrictChatMember.kt @@ -14,27 +14,31 @@ suspend fun TelegramBot.restrictChatMember( chatId: ChatIdentifier, userId: UserId, untilDate: TelegramDate? = null, - permissions: ChatPermissions = ChatPermissions() -) = execute(RestrictChatMember(chatId, userId, untilDate, permissions)) + permissions: ChatPermissions = ChatPermissions(), + useIndependentChatPermissions: Boolean? = permissions.isGranular.takeIf { it } +) = execute(RestrictChatMember(chatId, userId, untilDate, permissions, useIndependentChatPermissions)) suspend fun TelegramBot.restrictChatMember( chat: PublicChat, userId: UserId, untilDate: TelegramDate? = null, - permissions: ChatPermissions = ChatPermissions() -) = restrictChatMember(chat.id, userId, untilDate, permissions) + permissions: ChatPermissions = ChatPermissions(), + useIndependentChatPermissions: Boolean? = permissions.isGranular.takeIf { it } +) = restrictChatMember(chat.id, userId, untilDate, permissions, useIndependentChatPermissions) suspend fun TelegramBot.restrictChatMember( chatId: IdChatIdentifier, user: User, untilDate: TelegramDate? = null, - permissions: ChatPermissions = ChatPermissions() -) = restrictChatMember(chatId, user.id, untilDate, permissions) + permissions: ChatPermissions = ChatPermissions(), + useIndependentChatPermissions: Boolean? = permissions.isGranular.takeIf { it } +) = restrictChatMember(chatId, user.id, untilDate, permissions, useIndependentChatPermissions) suspend fun TelegramBot.restrictChatMember( chat: PublicChat, user: User, untilDate: TelegramDate? = null, - permissions: ChatPermissions = ChatPermissions() -) = restrictChatMember(chat.id, user.id, untilDate, permissions) + permissions: ChatPermissions = ChatPermissions(), + useIndependentChatPermissions: Boolean? = permissions.isGranular.takeIf { it } +) = restrictChatMember(chat.id, user.id, untilDate, permissions, useIndependentChatPermissions) diff --git a/tgbotapi.api/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/api/chat/modify/SetChatPermissions.kt b/tgbotapi.api/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/api/chat/modify/SetChatPermissions.kt index 1c6e3cb7db..1efc595e20 100644 --- a/tgbotapi.api/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/api/chat/modify/SetChatPermissions.kt +++ b/tgbotapi.api/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/api/chat/modify/SetChatPermissions.kt @@ -8,10 +8,12 @@ import dev.inmo.tgbotapi.types.chat.PublicChat suspend fun TelegramBot.setDefaultChatMembersPermissions( chatId: ChatIdentifier, - permissions: ChatPermissions -) = execute(SetChatPermissions(chatId, permissions)) + permissions: ChatPermissions, + useIndependentChatPermissions: Boolean? = permissions.isGranular.takeIf { it } +) = execute(SetChatPermissions(chatId, permissions, useIndependentChatPermissions)) suspend fun TelegramBot.setDefaultChatMembersPermissions( chat: PublicChat, - permissions: ChatPermissions -) = setDefaultChatMembersPermissions(chat.id, permissions) + permissions: ChatPermissions, + useIndependentChatPermissions: Boolean? = permissions.isGranular.takeIf { it } +) = setDefaultChatMembersPermissions(chat.id, permissions, useIndependentChatPermissions) diff --git a/tgbotapi.behaviour_builder.fsm/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/BehaviourContextWithFSMBuilder.kt b/tgbotapi.behaviour_builder.fsm/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/BehaviourContextWithFSMBuilder.kt index e090774496..f0efd52d45 100644 --- a/tgbotapi.behaviour_builder.fsm/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/BehaviourContextWithFSMBuilder.kt +++ b/tgbotapi.behaviour_builder.fsm/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/BehaviourContextWithFSMBuilder.kt @@ -8,6 +8,7 @@ import dev.inmo.micro_utils.fsm.common.utils.StateHandlingErrorHandler import dev.inmo.micro_utils.fsm.common.utils.defaultStateHandlingErrorHandler import dev.inmo.tgbotapi.bot.TelegramBot import dev.inmo.tgbotapi.extensions.utils.updates.retrieving.longPolling +import dev.inmo.tgbotapi.types.Seconds import dev.inmo.tgbotapi.types.update.abstracts.Update import dev.inmo.tgbotapi.updateshandlers.FlowsUpdatesFilter import kotlinx.coroutines.* @@ -54,6 +55,9 @@ suspend fun TelegramBot.buildBehaviourWithFSMAndStartLongPolling( statesManager: StatesManager = DefaultStatesManager(InMemoryDefaultStatesManagerRepo()), presetHandlers: List> = listOf(), onStateHandlingErrorHandler: StateHandlingErrorHandler = defaultStateHandlingErrorHandler(), + timeoutSeconds: Seconds = 30, + autoDisableWebhooks: Boolean = true, + autoSkipTimeoutExceptions: Boolean = true, block: CustomBehaviourContextReceiver, Unit> ): Pair, Job> = buildBehaviourWithFSM( upstreamUpdatesFlow, @@ -66,7 +70,7 @@ suspend fun TelegramBot.buildBehaviourWithFSMAndStartLongPolling( ).run { this to scope.launch { start() - longPolling(flowsUpdatesFilter, scope = scope) + longPolling(flowsUpdatesFilter, timeoutSeconds, scope, autoDisableWebhooks, autoSkipTimeoutExceptions, defaultExceptionsHandler) } } @@ -124,6 +128,9 @@ suspend fun TelegramBot.buildBehaviourWithFSMAndStartLongPolling( statesManager: StatesManager = DefaultStatesManager(InMemoryDefaultStatesManagerRepo()), presetHandlers: List> = listOf(), onStateHandlingErrorHandler: StateHandlingErrorHandler = defaultStateHandlingErrorHandler(), + timeoutSeconds: Seconds = 30, + autoDisableWebhooks: Boolean = true, + autoSkipTimeoutExceptions: Boolean = true, block: CustomBehaviourContextReceiver, Unit> ) = FlowsUpdatesFilter().let { buildBehaviourWithFSM( @@ -138,7 +145,11 @@ suspend fun TelegramBot.buildBehaviourWithFSMAndStartLongPolling( start() longPolling( flowsUpdatesFilter, - scope = scope + timeoutSeconds, + scope, + autoDisableWebhooks, + autoSkipTimeoutExceptions, + defaultExceptionsHandler ) } } diff --git a/tgbotapi.behaviour_builder.fsm/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/TelegramBotWithFSM.kt b/tgbotapi.behaviour_builder.fsm/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/TelegramBotWithFSM.kt index ca8a0a3253..97a1cd09a6 100644 --- a/tgbotapi.behaviour_builder.fsm/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/TelegramBotWithFSM.kt +++ b/tgbotapi.behaviour_builder.fsm/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/TelegramBotWithFSM.kt @@ -11,6 +11,7 @@ import dev.inmo.tgbotapi.bot.ktor.KtorRequestsExecutorBuilder import dev.inmo.tgbotapi.bot.ktor.telegramBot import dev.inmo.tgbotapi.bot.TelegramBot import dev.inmo.tgbotapi.extensions.utils.updates.retrieving.startGettingOfUpdatesByLongPolling +import dev.inmo.tgbotapi.types.Seconds import dev.inmo.tgbotapi.updateshandlers.FlowsUpdatesFilter import dev.inmo.tgbotapi.utils.telegramBotAPIDefaultUrl import kotlinx.coroutines.CoroutineScope @@ -42,6 +43,9 @@ suspend fun telegramBotWithBehaviourAndFSM( presetHandlers: List> = listOf(), testServer: Boolean = false, onStateHandlingErrorHandler: StateHandlingErrorHandler = defaultStateHandlingErrorHandler(), + timeoutSeconds: Seconds = 30, + autoDisableWebhooks: Boolean = true, + autoSkipTimeoutExceptions: Boolean = true, block: CustomBehaviourContextReceiver, Unit> ): TelegramBot = telegramBot( token, @@ -56,6 +60,9 @@ suspend fun telegramBotWithBehaviourAndFSM( statesManager, presetHandlers, onStateHandlingErrorHandler, + timeoutSeconds, + autoDisableWebhooks, + autoSkipTimeoutExceptions, block ) } @@ -81,6 +88,9 @@ suspend fun telegramBotWithBehaviourAndFSMAndStartLongPolling( presetHandlers: List> = listOf(), testServer: Boolean = false, onStateHandlingErrorHandler: StateHandlingErrorHandler = defaultStateHandlingErrorHandler(), + timeoutSeconds: Seconds = 30, + autoDisableWebhooks: Boolean = true, + autoSkipTimeoutExceptions: Boolean = true, block: CustomBehaviourContextReceiver, Unit> ): Pair { return telegramBot( @@ -95,6 +105,9 @@ suspend fun telegramBotWithBehaviourAndFSMAndStartLongPolling( statesManager, presetHandlers, onStateHandlingErrorHandler, + timeoutSeconds, + autoDisableWebhooks, + autoSkipTimeoutExceptions, block ) } diff --git a/tgbotapi.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/BehaviourBuilders.kt b/tgbotapi.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/BehaviourBuilders.kt index 8a8e0f91e6..730122b758 100644 --- a/tgbotapi.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/BehaviourBuilders.kt +++ b/tgbotapi.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/BehaviourBuilders.kt @@ -5,6 +5,7 @@ import dev.inmo.micro_utils.coroutines.ExceptionHandler import dev.inmo.tgbotapi.bot.TelegramBot import dev.inmo.tgbotapi.extensions.utils.updates.retrieving.longPolling import dev.inmo.tgbotapi.extensions.utils.updates.retrieving.startGettingOfUpdatesByLongPolling +import dev.inmo.tgbotapi.types.Seconds import dev.inmo.tgbotapi.updateshandlers.FlowsUpdatesFilter import kotlinx.coroutines.* @@ -53,6 +54,9 @@ suspend fun TelegramBot.buildBehaviour( suspend fun TelegramBot.buildBehaviourWithLongPolling( scope: CoroutineScope = defaultCoroutineScopeProvider(), defaultExceptionsHandler: ExceptionHandler? = null, + timeoutSeconds: Seconds = 30, + autoDisableWebhooks: Boolean = true, + autoSkipTimeoutExceptions: Boolean = true, block: BehaviourContextReceiver ): Job { val behaviourContext = buildBehaviour( @@ -62,6 +66,9 @@ suspend fun TelegramBot.buildBehaviourWithLongPolling( ) return longPolling( behaviourContext, - scope = behaviourContext + scope = behaviourContext, + timeoutSeconds = timeoutSeconds, + autoDisableWebhooks = autoDisableWebhooks, + autoSkipTimeoutExceptions = autoSkipTimeoutExceptions ) } diff --git a/tgbotapi.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/TelegramBot.kt b/tgbotapi.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/TelegramBot.kt index 19b06325c4..36c8df0ff9 100644 --- a/tgbotapi.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/TelegramBot.kt +++ b/tgbotapi.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/TelegramBot.kt @@ -5,6 +5,7 @@ import dev.inmo.tgbotapi.bot.TelegramBot import dev.inmo.tgbotapi.bot.ktor.KtorRequestsExecutorBuilder import dev.inmo.tgbotapi.bot.ktor.telegramBot import dev.inmo.tgbotapi.extensions.utils.updates.retrieving.startGettingOfUpdatesByLongPolling +import dev.inmo.tgbotapi.types.Seconds import dev.inmo.tgbotapi.updateshandlers.FlowsUpdatesFilter import dev.inmo.tgbotapi.utils.telegramBotAPIDefaultUrl import kotlinx.coroutines.* @@ -66,6 +67,9 @@ suspend fun telegramBotWithBehaviourAndLongPolling( builder: KtorRequestsExecutorBuilder.() -> Unit = {}, defaultExceptionsHandler: ExceptionHandler? = null, testServer: Boolean = false, + timeoutSeconds: Seconds = 30, + autoDisableWebhooks: Boolean = true, + autoSkipTimeoutExceptions: Boolean = true, block: BehaviourContextReceiver ): Pair { return telegramBot( @@ -77,6 +81,9 @@ suspend fun telegramBotWithBehaviourAndLongPolling( it to it.buildBehaviourWithLongPolling( scope ?: CoroutineScope(coroutineContext), defaultExceptionsHandler, + timeoutSeconds, + autoDisableWebhooks, + autoSkipTimeoutExceptions, block ) } diff --git a/tgbotapi.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/expectations/WaitEventAction.kt b/tgbotapi.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/expectations/WaitEventAction.kt index ec6e5f8572..2fb21d12f3 100644 --- a/tgbotapi.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/expectations/WaitEventAction.kt +++ b/tgbotapi.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/expectations/WaitEventAction.kt @@ -17,6 +17,9 @@ import dev.inmo.tgbotapi.types.message.ChatEvents.forum.WriteAccessAllowed import dev.inmo.tgbotapi.types.message.ChatEvents.voice.* import dev.inmo.tgbotapi.types.message.abstracts.ChatEventMessage import dev.inmo.tgbotapi.types.message.payments.SuccessfulPaymentEvent +import dev.inmo.tgbotapi.types.request.ChatShared +import dev.inmo.tgbotapi.types.request.ChatSharedRequest +import dev.inmo.tgbotapi.types.request.UserShared import dev.inmo.tgbotapi.utils.RiskFeature import dev.inmo.tgbotapi.utils.lowLevelRiskFeatureMessage import kotlinx.coroutines.flow.Flow @@ -171,3 +174,18 @@ suspend fun BehaviourContext.waitWriteAccessAllowed( initRequest: Request<*>? = null, errorFactory: NullableRequestBuilder<*> = { null } ) = waitEvents(initRequest, errorFactory) + +suspend fun BehaviourContext.waitChatSharedRequest( + initRequest: Request<*>? = null, + errorFactory: NullableRequestBuilder<*> = { null } +) = waitEvents(initRequest, errorFactory) + +suspend fun BehaviourContext.waitUserShared( + initRequest: Request<*>? = null, + errorFactory: NullableRequestBuilder<*> = { null } +) = waitEvents(initRequest, errorFactory) + +suspend fun BehaviourContext.waitChatShared( + initRequest: Request<*>? = null, + errorFactory: NullableRequestBuilder<*> = { null } +) = waitEvents(initRequest, errorFactory) diff --git a/tgbotapi.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/expectations/WaitEventActionMessages.kt b/tgbotapi.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/expectations/WaitEventActionMessages.kt index a26a4215ad..9539f3d2fd 100644 --- a/tgbotapi.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/expectations/WaitEventActionMessages.kt +++ b/tgbotapi.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/expectations/WaitEventActionMessages.kt @@ -9,10 +9,17 @@ import dev.inmo.tgbotapi.types.message.ChatEvents.* import dev.inmo.tgbotapi.types.message.ChatEvents.abstracts.* import dev.inmo.tgbotapi.types.message.ChatEvents.forum.ForumTopicClosed import dev.inmo.tgbotapi.types.message.ChatEvents.forum.ForumTopicCreated +import dev.inmo.tgbotapi.types.message.ChatEvents.forum.ForumTopicEdited import dev.inmo.tgbotapi.types.message.ChatEvents.forum.ForumTopicReopened +import dev.inmo.tgbotapi.types.message.ChatEvents.forum.GeneralForumTopicHidden +import dev.inmo.tgbotapi.types.message.ChatEvents.forum.GeneralForumTopicUnhidden +import dev.inmo.tgbotapi.types.message.ChatEvents.forum.WriteAccessAllowed import dev.inmo.tgbotapi.types.message.ChatEvents.voice.* import dev.inmo.tgbotapi.types.message.abstracts.ChatEventMessage import dev.inmo.tgbotapi.types.message.payments.SuccessfulPaymentEvent +import dev.inmo.tgbotapi.types.request.ChatShared +import dev.inmo.tgbotapi.types.request.ChatSharedRequest +import dev.inmo.tgbotapi.types.request.UserShared import dev.inmo.tgbotapi.utils.RiskFeature import dev.inmo.tgbotapi.utils.lowLevelRiskFeatureMessage import kotlinx.coroutines.flow.Flow @@ -148,3 +155,34 @@ suspend fun BehaviourContext.waitForumTopicReopenedEventsMessages( initRequest: Request<*>? = null, errorFactory: NullableRequestBuilder<*> = { null } ) = waitEventsMessages(initRequest, errorFactory) +suspend fun BehaviourContext.waitForumTopicEditedEventsMessages( + initRequest: Request<*>? = null, + errorFactory: NullableRequestBuilder<*> = { null } +) = waitEventsMessages(initRequest, errorFactory) +suspend fun BehaviourContext.waitGeneralForumTopicHiddenEventsMessages( + initRequest: Request<*>? = null, + errorFactory: NullableRequestBuilder<*> = { null } +) = waitEventsMessages(initRequest, errorFactory) +suspend fun BehaviourContext.waitGeneralForumTopicUnhiddenEventsMessages( + initRequest: Request<*>? = null, + errorFactory: NullableRequestBuilder<*> = { null } +) = waitEventsMessages(initRequest, errorFactory) +suspend fun BehaviourContext.waitWriteAccessAllowedEventsMessages( + initRequest: Request<*>? = null, + errorFactory: NullableRequestBuilder<*> = { null } +) = waitEventsMessages(initRequest, errorFactory) + +suspend fun BehaviourContext.waitChatSharedRequestEventsMessages( + initRequest: Request<*>? = null, + errorFactory: NullableRequestBuilder<*> = { null } +) = waitEventsMessages(initRequest, errorFactory) + +suspend fun BehaviourContext.waitUserSharedEventsMessages( + initRequest: Request<*>? = null, + errorFactory: NullableRequestBuilder<*> = { null } +) = waitEventsMessages(initRequest, errorFactory) + +suspend fun BehaviourContext.waitChatSharedEventsMessages( + initRequest: Request<*>? = null, + errorFactory: NullableRequestBuilder<*> = { null } +) = waitEventsMessages(initRequest, errorFactory) diff --git a/tgbotapi.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/triggers_handling/EventTriggers.kt b/tgbotapi.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/triggers_handling/EventTriggers.kt index 06662e8a59..645ac7effb 100644 --- a/tgbotapi.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/triggers_handling/EventTriggers.kt +++ b/tgbotapi.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/triggers_handling/EventTriggers.kt @@ -23,6 +23,9 @@ import dev.inmo.tgbotapi.types.message.PrivateEventMessage import dev.inmo.tgbotapi.types.message.abstracts.ChatEventMessage import dev.inmo.tgbotapi.types.message.abstracts.SupergroupEventMessage import dev.inmo.tgbotapi.types.message.payments.SuccessfulPaymentEvent +import dev.inmo.tgbotapi.types.request.ChatShared +import dev.inmo.tgbotapi.types.request.ChatSharedRequest +import dev.inmo.tgbotapi.types.request.UserShared import dev.inmo.tgbotapi.types.update.abstracts.Update internal suspend inline fun BC.onEvent( @@ -657,3 +660,65 @@ suspend fun BC.onWriteAccessAllowed( markerFactory: MarkerFactory, Any> = ByChatMessageMarkerFactory, scenarioReceiver: CustomBehaviourContextAndTypeReceiver> ) = onEventWithCustomChatEventMessage(initialFilter, subcontextUpdatesFilter, markerFactory, scenarioReceiver) + + + +/** + * @param initialFilter This filter will be called to remove unnecessary data BEFORE [scenarioReceiver] call + * @param subcontextUpdatesFilter This filter will be applied to each update inside of [scenarioReceiver]. For example, + * this filter will be used if you will call [dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitContentMessage]. + * Use [dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContextAndTwoTypesReceiver] function to create your own. + * Use [dev.inmo.tgbotapi.extensions.behaviour_builder.utils.plus] or [dev.inmo.tgbotapi.extensions.behaviour_builder.utils.times] + * to combinate several filters + * @param markerFactory Will be used to identify different "stream". [scenarioReceiver] will be called synchronously + * in one "stream". Output of [markerFactory] will be used as a key for "stream" + * @param scenarioReceiver Main callback which will be used to handle incoming data if [initialFilter] will pass that + * data + */ +suspend fun BC.onChatSharedRequest( + initialFilter: SimpleFilter>? = null, + subcontextUpdatesFilter: CustomBehaviourContextAndTwoTypesReceiver, Update>? = MessageFilterByChat, + markerFactory: MarkerFactory, Any> = ByChatMessageMarkerFactory, + scenarioReceiver: CustomBehaviourContextAndTypeReceiver> +) = onEventWithCustomChatEventMessage(initialFilter, subcontextUpdatesFilter, markerFactory, scenarioReceiver) + + +/** + * @param initialFilter This filter will be called to remove unnecessary data BEFORE [scenarioReceiver] call + * @param subcontextUpdatesFilter This filter will be applied to each update inside of [scenarioReceiver]. For example, + * this filter will be used if you will call [dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitContentMessage]. + * Use [dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContextAndTwoTypesReceiver] function to create your own. + * Use [dev.inmo.tgbotapi.extensions.behaviour_builder.utils.plus] or [dev.inmo.tgbotapi.extensions.behaviour_builder.utils.times] + * to combinate several filters + * @param markerFactory Will be used to identify different "stream". [scenarioReceiver] will be called synchronously + * in one "stream". Output of [markerFactory] will be used as a key for "stream" + * @param scenarioReceiver Main callback which will be used to handle incoming data if [initialFilter] will pass that + * data + */ +suspend fun BC.onUserShared( + initialFilter: SimpleFilter>? = null, + subcontextUpdatesFilter: CustomBehaviourContextAndTwoTypesReceiver, Update>? = MessageFilterByChat, + markerFactory: MarkerFactory, Any> = ByChatMessageMarkerFactory, + scenarioReceiver: CustomBehaviourContextAndTypeReceiver> +) = onEventWithCustomChatEventMessage(initialFilter, subcontextUpdatesFilter, markerFactory, scenarioReceiver) + + + +/** + * @param initialFilter This filter will be called to remove unnecessary data BEFORE [scenarioReceiver] call + * @param subcontextUpdatesFilter This filter will be applied to each update inside of [scenarioReceiver]. For example, + * this filter will be used if you will call [dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitContentMessage]. + * Use [dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContextAndTwoTypesReceiver] function to create your own. + * Use [dev.inmo.tgbotapi.extensions.behaviour_builder.utils.plus] or [dev.inmo.tgbotapi.extensions.behaviour_builder.utils.times] + * to combinate several filters + * @param markerFactory Will be used to identify different "stream". [scenarioReceiver] will be called synchronously + * in one "stream". Output of [markerFactory] will be used as a key for "stream" + * @param scenarioReceiver Main callback which will be used to handle incoming data if [initialFilter] will pass that + * data + */ +suspend fun BC.onChatShared( + initialFilter: SimpleFilter>? = null, + subcontextUpdatesFilter: CustomBehaviourContextAndTwoTypesReceiver, Update>? = MessageFilterByChat, + markerFactory: MarkerFactory, Any> = ByChatMessageMarkerFactory, + scenarioReceiver: CustomBehaviourContextAndTypeReceiver> +) = onEventWithCustomChatEventMessage(initialFilter, subcontextUpdatesFilter, markerFactory, scenarioReceiver) diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/requests/chat/members/RestrictChatMember.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/requests/chat/members/RestrictChatMember.kt index 92e3dc2458..bdb9cc5afe 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/requests/chat/members/RestrictChatMember.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/requests/chat/members/RestrictChatMember.kt @@ -16,7 +16,9 @@ data class RestrictChatMember( @SerialName(untilDateField) override val untilDate: TelegramDate? = null, @SerialName(permissionsField) - val permissions: ChatPermissions = ChatPermissions() + val permissions: ChatPermissions = ChatPermissions(), + @SerialName(useIndependentChatPermissionsField) + val useIndependentChatPermissions: Boolean? = permissions.isGranular.takeIf { it } ) : ChatMemberRequest, UntilDate { override fun method(): String = "restrictChatMember" override val resultDeserializer: DeserializationStrategy diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/requests/chat/modify/SetChatPermissions.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/requests/chat/modify/SetChatPermissions.kt index 04e292771b..6d854aa572 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/requests/chat/modify/SetChatPermissions.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/requests/chat/modify/SetChatPermissions.kt @@ -12,7 +12,9 @@ data class SetChatPermissions ( @SerialName(chatIdField) override val chatId: ChatIdentifier, @SerialName(permissionsField) - val permissions: ChatPermissions + val permissions: ChatPermissions, + @SerialName(useIndependentChatPermissionsField) + val useIndependentChatPermissions: Boolean? = permissions.isGranular.takeIf { it } ): ChatRequest, SimpleRequest { override fun method(): String = "setChatPermissions" override val resultDeserializer: DeserializationStrategy diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/Common.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/Common.kt index 8a3d8b8d85..4dada53823 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/Common.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/Common.kt @@ -261,6 +261,19 @@ const val iconColorField = "icon_color" const val requestContactField = "request_contact" const val requestLocationField = "request_location" const val requestPollField = "request_poll" +const val requestUserField = "request_user" +const val requestChatField = "request_chat" +const val requestIdField = "request_id" + +const val userIsBotField = "user_is_bot" +const val userIsPremiumField = "user_is_premium" +const val chatIsChannelField = "chat_is_channel" +const val chatIsForumField = "chat_is_forum" +const val chatHasUsernameField = "chat_has_username" +const val chatIsCreatedField = "chat_is_created" +const val userAdministratorRightsField = "user_administrator_rights" +const val botAdministratorRightsField = "bot_administrator_rights" +const val botIsMemberField = "bot_is_member" const val fileNameField = "file_name" const val mimeTypeField = "mime_type" @@ -327,7 +340,12 @@ const val scopeField = "scope" const val isMemberField = "is_member" const val isForumField = "is_forum" const val canSendMessagesField = "can_send_messages" -const val canSendMediaMessagesField = "can_send_media_messages" +const val canSendAudiosField = "can_send_audios" +const val canSendDocumentsField = "can_send_documents" +const val canSendPhotosField = "can_send_photos" +const val canSendVideosField = "can_send_videos" +const val canSendVideoNotesField = "can_send_video_notes" +const val canSendVoiceNotesField = "can_send_voice_notes" const val canSendOtherMessagesField = "can_send_other_messages" const val canSendPollsField = "can_send_polls" const val canAddWebPagePreviewsField = "can_add_web_page_previews" @@ -345,6 +363,7 @@ const val canPinMessagesField = "can_pin_messages" const val canPromoteMembersField = "can_promote_members" const val canManageVoiceChatsField = "can_manage_voice_chats" const val canManageVideoChatsField = "can_manage_video_chats" +const val useIndependentChatPermissionsField = "use_independent_chat_permissions" const val rightsField = "rights" const val forChannelsField = "for_channels" const val canManageChatField = "can_manage_chat" @@ -375,6 +394,7 @@ const val latitudeField = "latitude" const val longitudeField = "longitude" const val headingField = "heading" const val fromField = "from" +const val userChatIdField = "user_chat_id" const val userField = "user" const val dateField = "date" const val chatField = "chat" diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/buttons/KeyboardButton.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/buttons/KeyboardButton.kt index 4226bc94d2..64c326637d 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/buttons/KeyboardButton.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/buttons/KeyboardButton.kt @@ -94,6 +94,42 @@ data class RequestPollKeyboardButton( val requestPoll: KeyboardButtonPollType ) : KeyboardButton +/** + * Private chats only. When user will tap on this button, he will be asked for the chat with [requestChat] options. You + * will be able to catch this [ChatId] in updates and data using + * [dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onUserShared] in case you are using Behaviour + * Builder OR with [dev.inmo.tgbotapi.updateshandlers.FlowsUpdatesFilter.messagesFlow] + * and [kotlinx.coroutines.flow.filterIsInstance]. + * + * In case you will use [dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onUserShared] it is + * recommended to use [kotlinx.coroutines.flow.Flow] [kotlinx.coroutines.flow.filter] with checking of incoming + * [dev.inmo.tgbotapi.types.request.UserShared.requestId] + */ +@Serializable +data class RequestUserKeyboardButton( + override val text: String, + @SerialName(requestUserField) + val requestUser: KeyboardButtonRequestUser +) : KeyboardButton + +/** + * Private chats only. When user will tap on this button, he will be asked for the chat with [requestChat] options. You + * will be able to catch this [ChatId] in updates and data using + * [dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onChatShared] in case you are using Behaviour + * Builder OR with [dev.inmo.tgbotapi.updateshandlers.FlowsUpdatesFilter.messagesFlow] + * and [kotlinx.coroutines.flow.filterIsInstance]. + * + * In case you will use [dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onChatShared] it is + * recommended to use [kotlinx.coroutines.flow.Flow] [kotlinx.coroutines.flow.filter] with checking of incoming + * [dev.inmo.tgbotapi.types.request.ChatShared.requestId] + */ +@Serializable +data class RequestChatKeyboardButton( + override val text: String, + @SerialName(requestChatField) + val requestChat: KeyboardButtonRequestChat +) : KeyboardButton + @RiskFeature object KeyboardButtonSerializer : KSerializer { private val internalSerializer = JsonElement.serializer() @@ -124,6 +160,20 @@ object KeyboardButtonSerializer : KSerializer { asJson[requestPollField] ?.jsonObject ?: buildJsonObject { } ) ) + asJson is JsonObject && asJson[requestUserField] != null -> RequestUserKeyboardButton( + asJson[textField]!!.jsonPrimitive.content, + nonstrictJsonFormat.decodeFromJsonElement( + KeyboardButtonRequestUser.serializer(), + asJson[requestUserField] ?.jsonObject ?: buildJsonObject { } + ) + ) + asJson is JsonObject && asJson[requestChatField] != null -> RequestChatKeyboardButton( + asJson[textField]!!.jsonPrimitive.content, + nonstrictJsonFormat.decodeFromJsonElement( + KeyboardButtonRequestChat.serializer(), + asJson[requestChatField] ?.jsonObject ?: buildJsonObject { } + ) + ) else -> UnknownKeyboardButton( when (asJson) { is JsonObject -> asJson[textField]!!.jsonPrimitive.content @@ -142,6 +192,8 @@ object KeyboardButtonSerializer : KSerializer { is WebAppKeyboardButton -> WebAppKeyboardButton.serializer().serialize(encoder, value) is RequestPollKeyboardButton -> RequestPollKeyboardButton.serializer().serialize(encoder, value) is SimpleKeyboardButton -> encoder.encodeString(value.text) + is RequestUserKeyboardButton -> RequestUserKeyboardButton.serializer().serialize(encoder, value) + is RequestChatKeyboardButton -> RequestChatKeyboardButton.serializer().serialize(encoder, value) is UnknownKeyboardButton -> JsonElement.serializer().serialize(encoder, nonstrictJsonFormat.parseToJsonElement(value.raw)) } } diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/buttons/KeyboardButtonRequestChat.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/buttons/KeyboardButtonRequestChat.kt new file mode 100644 index 0000000000..fccb2c64ce --- /dev/null +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/buttons/KeyboardButtonRequestChat.kt @@ -0,0 +1,78 @@ +package dev.inmo.tgbotapi.types.buttons + +import dev.inmo.tgbotapi.types.botAdministratorRightsField +import dev.inmo.tgbotapi.types.botIsMemberField +import dev.inmo.tgbotapi.types.chat.member.ChatAdministratorRights +import dev.inmo.tgbotapi.types.chatHasUsernameField +import dev.inmo.tgbotapi.types.chatIsChannelField +import dev.inmo.tgbotapi.types.chatIsCreatedField +import dev.inmo.tgbotapi.types.chatIsForumField +import dev.inmo.tgbotapi.types.request.RequestId +import dev.inmo.tgbotapi.types.requestIdField +import dev.inmo.tgbotapi.types.userAdministratorRightsField +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * @see Channel + * @see Group + */ +@Serializable +data class KeyboardButtonRequestChat( + @SerialName(requestIdField) + val requestId: RequestId, + @SerialName(chatIsChannelField) + val isChannel: Boolean? = null, + @SerialName(chatIsForumField) + val isForum: Boolean? = null, + @SerialName(chatHasUsernameField) + val isPublic: Boolean? = null, + @SerialName(chatIsCreatedField) + val isOwnedBy: Boolean? = null, + @SerialName(userAdministratorRightsField) + val userRightsInChat: ChatAdministratorRights? = null, + @SerialName(botAdministratorRightsField) + val botRightsInChat: ChatAdministratorRights? = null, + @SerialName(botIsMemberField) + val botIsMember: Boolean? = null +) { + companion object { + fun Channel( + requestId: RequestId, + isPublic: Boolean? = null, + isOwnedBy: Boolean? = null, + userRightsInChat: ChatAdministratorRights? = null, + botRightsInChat: ChatAdministratorRights? = null, + botIsMember: Boolean? = null + ) = KeyboardButtonRequestChat( + requestId = requestId, + isChannel = true, + isForum = null, + isPublic = isPublic, + isOwnedBy = isOwnedBy, + userRightsInChat = userRightsInChat, + botRightsInChat = botRightsInChat, + botIsMember = botIsMember + ) + + fun Group( + requestId: RequestId, + isForum: Boolean? = null, + isPublic: Boolean? = null, + isOwnedBy: Boolean? = null, + userRightsInChat: ChatAdministratorRights? = null, + botRightsInChat: ChatAdministratorRights? = null, + botIsMember: Boolean? = null + ) = KeyboardButtonRequestChat( + requestId = requestId, + isChannel = false, + isForum = isForum, + isPublic = isPublic, + isOwnedBy = isOwnedBy, + userRightsInChat = userRightsInChat, + botRightsInChat = botRightsInChat, + botIsMember = botIsMember + ) + } +} + diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/buttons/KeyboardButtonRequestUser.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/buttons/KeyboardButtonRequestUser.kt new file mode 100644 index 0000000000..99169842ad --- /dev/null +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/buttons/KeyboardButtonRequestUser.kt @@ -0,0 +1,91 @@ +package dev.inmo.tgbotapi.types.buttons + +import dev.inmo.tgbotapi.types.request.RequestId +import dev.inmo.tgbotapi.types.requestIdField +import dev.inmo.tgbotapi.types.userIsBotField +import dev.inmo.tgbotapi.types.userIsPremiumField +import dev.inmo.tgbotapi.utils.internal.ClassCastsIncluded +import kotlinx.serialization.EncodeDefault +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.Serializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +@Serializable(KeyboardButtonRequestUser.Companion::class) +@ClassCastsIncluded +sealed interface KeyboardButtonRequestUser { + val requestId: RequestId + val isBot: Boolean? + + @Serializable + data class Any( + @SerialName(requestIdField) + override val requestId: RequestId + ) : KeyboardButtonRequestUser { + @SerialName(userIsBotField) + @EncodeDefault + override val isBot: Boolean? = null + } + + @Serializable + data class Common( + @SerialName(requestIdField) + override val requestId: RequestId, + @SerialName(userIsPremiumField) + val isPremium: Boolean? = null + ) : KeyboardButtonRequestUser { + @SerialName(userIsBotField) + @EncodeDefault + override val isBot: Boolean = false + } + + @Serializable + data class Bot( + @SerialName(requestIdField) + override val requestId: RequestId + ) : KeyboardButtonRequestUser { + @SerialName(userIsBotField) + @EncodeDefault + override val isBot: Boolean = true + } + + @Serializer(KeyboardButtonRequestUser::class) + companion object : KSerializer { + @Serializable + private data class Surrogate( + @SerialName(requestIdField) + val requestId: RequestId, + @SerialName(userIsBotField) + val userIsBot: Boolean? = null, + @SerialName(userIsPremiumField) + val userIsPremium: Boolean? = null + ) + private val realSerializer = Surrogate.serializer() + + override val descriptor: SerialDescriptor = realSerializer.descriptor + + override fun deserialize(decoder: Decoder): KeyboardButtonRequestUser { + val surrogate = realSerializer.deserialize(decoder) + + return when (surrogate.userIsBot) { + true -> Bot(surrogate.requestId) + false -> Common(surrogate.requestId, surrogate.userIsPremium) + null -> Any(surrogate.requestId) + } + } + + override fun serialize(encoder: Encoder, value: KeyboardButtonRequestUser) { + realSerializer.serialize( + encoder, + Surrogate( + value.requestId, + value.isBot, + (value as? Common) ?.isPremium + ) + ) + } + } +} diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/buttons/reply/ReplyKeyboardButtonsShortcuts.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/buttons/reply/ReplyKeyboardButtonsShortcuts.kt index d87ed524f4..434faf7723 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/buttons/reply/ReplyKeyboardButtonsShortcuts.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/buttons/reply/ReplyKeyboardButtonsShortcuts.kt @@ -2,6 +2,8 @@ package dev.inmo.tgbotapi.types.buttons.reply import dev.inmo.tgbotapi.types.buttons.* import dev.inmo.tgbotapi.types.buttons.InlineKeyboardButtons.* +import dev.inmo.tgbotapi.types.chat.member.ChatAdministratorRights +import dev.inmo.tgbotapi.types.request.RequestId import dev.inmo.tgbotapi.types.webapps.WebAppInfo @@ -67,3 +69,161 @@ inline fun webAppReplyButton( text: String, url: String ) = webAppReplyButton(text, WebAppInfo(url)) + + +/** + * Creates and put [RequestUserKeyboardButton] + * + * @see replyKeyboard + * @see ReplyKeyboardBuilder.row + */ +inline fun requestUserReplyButton( + text: String, + requestUser: KeyboardButtonRequestUser +) = RequestUserKeyboardButton( + text, + requestUser +) + +/** + * Creates and put [RequestUserKeyboardButton] with [KeyboardButtonRequestUser.Bot] + * + * @see replyKeyboard + * @see ReplyKeyboardBuilder.row + */ +inline fun requestBotReplyButton( + text: String, + requestId: RequestId +) = requestUserReplyButton( + text, + KeyboardButtonRequestUser.Bot(requestId) +) + +/** + * Creates and put [RequestUserKeyboardButton] with [KeyboardButtonRequestUser.Common] + * + * @see replyKeyboard + * @see ReplyKeyboardBuilder.row + */ +inline fun requestUserReplyButton( + text: String, + requestId: RequestId, + premiumUser: Boolean? = null +) = requestUserReplyButton( + text, + KeyboardButtonRequestUser.Common(requestId, premiumUser) +) + +/** + * Creates and put [RequestUserKeyboardButton] with [KeyboardButtonRequestUser.Any] + * + * @see replyKeyboard + * @see ReplyKeyboardBuilder.row + */ +inline fun requestUserOrBotReplyButton( + text: String, + requestId: RequestId +) = requestUserReplyButton( + text, + KeyboardButtonRequestUser.Any(requestId) +) + + +/** + * Creates and put [RequestChatKeyboardButton] + * + * @see replyKeyboard + * @see ReplyKeyboardBuilder.row + */ +inline fun requestChatReplyButton( + text: String, + requestChat: KeyboardButtonRequestChat +) = RequestChatKeyboardButton( + text, + requestChat +) + +/** + * Creates and put [RequestChatKeyboardButton] with [KeyboardButtonRequestChat] + * + * @see replyKeyboard + * @see ReplyKeyboardBuilder.row + */ +inline fun requestChatReplyButton( + text: String, + requestId: RequestId, + isChannel: Boolean? = null, + isForum: Boolean? = null, + isPublic: Boolean? = null, + isOwnedBy: Boolean? = null, + userRightsInChat: ChatAdministratorRights? = null, + botRightsInChat: ChatAdministratorRights? = null, + botIsMember: Boolean = false +) = requestChatReplyButton( + text, + KeyboardButtonRequestChat( + requestId = requestId, + isChannel = isChannel, + isForum = isForum, + isPublic = isPublic, + isOwnedBy = isOwnedBy, + userRightsInChat = userRightsInChat, + botRightsInChat = botRightsInChat, + botIsMember = botIsMember + ) +) + +/** + * Creates and put [RequestChatKeyboardButton] with [KeyboardButtonRequestChat.Channel] + * + * @see replyKeyboard + * @see ReplyKeyboardBuilder.row + */ +inline fun requestChannelReplyButton( + text: String, + requestId: RequestId, + isPublic: Boolean? = null, + isOwnedBy: Boolean? = null, + userRightsInChat: ChatAdministratorRights? = null, + botRightsInChat: ChatAdministratorRights? = null, + botIsMember: Boolean = false +) = requestChatReplyButton( + text, + KeyboardButtonRequestChat.Channel( + requestId = requestId, + isPublic = isPublic, + isOwnedBy = isOwnedBy, + userRightsInChat = userRightsInChat, + botRightsInChat = botRightsInChat, + botIsMember = botIsMember + ) +) + + +/** + * Creates and put [RequestChatKeyboardButton] with [KeyboardButtonRequestChat.Group] + * + * @see replyKeyboard + * @see ReplyKeyboardBuilder.row + */ +inline fun requestChannelReplyButton( + text: String, + requestId: RequestId, + isForum: Boolean? = null, + isPublic: Boolean? = null, + isOwnedBy: Boolean? = null, + userRightsInChat: ChatAdministratorRights? = null, + botRightsInChat: ChatAdministratorRights? = null, + botIsMember: Boolean? = null +) = requestChatReplyButton( + text, + KeyboardButtonRequestChat.Group( + requestId = requestId, + isForum = isForum, + isPublic = isPublic, + isOwnedBy = isOwnedBy, + userRightsInChat = userRightsInChat, + botRightsInChat = botRightsInChat, + botIsMember = botIsMember + ) +) diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/chat/ChatJoinRequest.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/chat/ChatJoinRequest.kt index 738e95adc6..90a00b33f7 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/chat/ChatJoinRequest.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/chat/ChatJoinRequest.kt @@ -15,6 +15,8 @@ data class ChatJoinRequest( val chat: PublicChat, @SerialName(fromField) override val from: User, + @SerialName(userChatIdField) + val userChatId: UserId, @SerialName(dateField) val date: TelegramDate, @SerialName(inviteLinkField) diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/chat/ChatPermissions.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/chat/ChatPermissions.kt index 67091d9bd4..bd8f67f218 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/chat/ChatPermissions.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/chat/ChatPermissions.kt @@ -1,32 +1,259 @@ package dev.inmo.tgbotapi.types.chat import dev.inmo.tgbotapi.types.* +import kotlinx.serialization.KSerializer import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder -@Serializable -data class ChatPermissions( - @SerialName(canSendMessagesField) - val canSendMessages: Boolean = false, - @SerialName(canSendMediaMessagesField) - val canSendMediaMessages: Boolean = false, - @SerialName(canSendPollsField) - val canSendPolls: Boolean = false, - @SerialName(canSendOtherMessagesField) - val canSendOtherMessages: Boolean = false, - @SerialName(canAddWebPagePreviewsField) - val canAddWebPagePreviews: Boolean = false, - @SerialName(canChangeInfoField) - val canChangeInfo: Boolean = false, - @SerialName(canInviteUsersField) - val canInviteUsers: Boolean = false, - @SerialName(canPinMessagesField) - val canPinMessages: Boolean = false -) +/** + * Represents any type with common permissions list + * + * !!WARNING!! Default serializer of this interface is using [Granular] as surrogate and in fact serialized + * and deserialized as [Granular]. In case you wish some custom behaviour you must implement your own + * [KSerializer] or pass another serializer + */ +@Serializable(ChatPermissions.Companion::class) +interface ChatPermissions { + val canSendMessages: Boolean? + val canSendAudios: Boolean? + val canSendDocuments: Boolean? + val canSendPhotos: Boolean? + val canSendVideos: Boolean? + val canSendVideoNotes: Boolean? + val canSendVoiceNotes: Boolean? + val canSendPolls: Boolean? + val canSendOtherMessages: Boolean? + val canAddWebPagePreviews: Boolean? + val canChangeInfo: Boolean? + val canInviteUsers: Boolean? + val canPinMessages: Boolean? + @Transient + val isGranular + get() = canSendAudios != null || + canSendDocuments != null || + canSendVideoNotes != null || + canSendPhotos != null || + canSendVideos != null || + canSendVoiceNotes != null + val canSendStickers: Boolean? + get() = canSendOtherMessages + val canSendGifs: Boolean? + get() = canSendStickers + + @Serializable + data class Granular( + @SerialName(canSendMessagesField) + override val canSendMessages: Boolean? = null, + @SerialName(canSendAudiosField) + override val canSendAudios: Boolean? = null, + @SerialName(canSendDocumentsField) + override val canSendDocuments: Boolean? = null, + @SerialName(canSendPhotosField) + override val canSendPhotos: Boolean? = null, + @SerialName(canSendVideosField) + override val canSendVideos: Boolean? = null, + @SerialName(canSendVideoNotesField) + override val canSendVideoNotes: Boolean? = null, + @SerialName(canSendVoiceNotesField) + override val canSendVoiceNotes: Boolean? = null, + @SerialName(canSendPollsField) + override val canSendPolls: Boolean? = null, + @SerialName(canSendOtherMessagesField) + override val canSendOtherMessages: Boolean? = null, + @SerialName(canAddWebPagePreviewsField) + override val canAddWebPagePreviews: Boolean? = null, + @SerialName(canChangeInfoField) + override val canChangeInfo: Boolean? = null, + @SerialName(canInviteUsersField) + override val canInviteUsers: Boolean? = null, + @SerialName(canPinMessagesField) + override val canPinMessages: Boolean? = null + ) : ChatPermissions { + @Transient + override val isGranular: Boolean + get() = true + } + + @Serializable + data class Common( + @SerialName(canSendPollsField) + override val canSendPolls: Boolean? = null, + @SerialName(canSendOtherMessagesField) + override val canSendOtherMessages: Boolean? = null, + @SerialName(canAddWebPagePreviewsField) + override val canAddWebPagePreviews: Boolean? = null, + @SerialName(canChangeInfoField) + override val canChangeInfo: Boolean? = null, + @SerialName(canInviteUsersField) + override val canInviteUsers: Boolean? = null, + @SerialName(canPinMessagesField) + override val canPinMessages: Boolean? = null + ) : ChatPermissions { + @Transient + override val isGranular: Boolean + get() = false + @Transient + override val canSendMessages: Boolean? = canSendOtherMessages ?.let { + it && (canAddWebPagePreviews ?: return@let null) + } + @Transient + override val canSendAudios: Boolean? + get() = canSendMessages + @Transient + override val canSendDocuments: Boolean? + get() = canSendMessages + @Transient + override val canSendPhotos: Boolean? + get() = canSendMessages + @Transient + override val canSendVideos: Boolean? + get() = canSendMessages + @Transient + override val canSendVideoNotes: Boolean? + get() = canSendMessages + @Transient + override val canSendVoiceNotes: Boolean? + get() = canSendMessages + } + + companion object : KSerializer { + operator fun invoke( + canSendMessages: Boolean? = null, + canSendAudios: Boolean? = null, + canSendDocuments: Boolean? = null, + canSendPhotos: Boolean? = null, + canSendVideos: Boolean? = null, + canSendVideoNotes: Boolean? = null, + canSendVoiceNotes: Boolean? = null, + canSendPolls: Boolean? = null, + canSendOtherMessages: Boolean? = null, + canAddWebPagePreviews: Boolean? = null, + canChangeInfo: Boolean? = null, + canInviteUsers: Boolean? = null, + canPinMessages: Boolean? = null + ) = Granular( + canSendMessages = canSendMessages, + canSendAudios = canSendAudios, + canSendDocuments = canSendDocuments, + canSendPhotos = canSendPhotos, + canSendVideos = canSendVideos, + canSendVideoNotes = canSendVideoNotes, + canSendVoiceNotes = canSendVoiceNotes, + canSendPolls = canSendPolls, + canSendOtherMessages = canSendOtherMessages, + canAddWebPagePreviews = canAddWebPagePreviews, + canChangeInfo = canChangeInfo, + canInviteUsers = canInviteUsers, + canPinMessages = canPinMessages + ) + + private val realSerializer = Granular.serializer() + private val commonSerializer = Common.serializer() + override val descriptor: SerialDescriptor + get() = realSerializer.descriptor + + override fun deserialize(decoder: Decoder): ChatPermissions { + return realSerializer.deserialize(decoder) + } + + override fun serialize(encoder: Encoder, value: ChatPermissions) { + realSerializer.serialize( + encoder, + (value as? Granular) ?: value.run { + Granular( + canSendMessages = canSendMessages, + canSendAudios = canSendAudios, + canSendDocuments = canSendDocuments, + canSendPhotos = canSendPhotos, + canSendVideos = canSendVideos, + canSendVideoNotes = canSendVideoNotes, + canSendVoiceNotes = canSendVoiceNotes, + canSendPolls = canSendPolls, + canSendOtherMessages = canSendOtherMessages, + canAddWebPagePreviews = canAddWebPagePreviews, + canChangeInfo = canChangeInfo, + canInviteUsers = canInviteUsers, + canPinMessages = canPinMessages + ) + } + ) + } + } + + /** + * Copying current instance as [ChatPermissions], but realizations of this interface may differently override this + * method + */ + fun copyGranular( + canSendMessages: Boolean? = this.canSendMessages, + canSendAudios: Boolean? = this.canSendAudios, + canSendDocuments: Boolean? = this.canSendDocuments, + canSendPhotos: Boolean? = this.canSendPhotos, + canSendVideos: Boolean? = this.canSendVideos, + canSendVideoNotes: Boolean? = this.canSendVideoNotes, + canSendVoiceNotes: Boolean? = this.canSendVoiceNotes, + canSendPolls: Boolean? = this.canSendPolls, + canSendOtherMessages: Boolean? = this.canSendOtherMessages, + canAddWebPagePreviews: Boolean? = this.canAddWebPagePreviews, + canChangeInfo: Boolean? = this.canChangeInfo, + canInviteUsers: Boolean? = this.canInviteUsers, + canPinMessages: Boolean? = this.canPinMessages + ): ChatPermissions = ChatPermissions( + canSendMessages = canSendMessages, + canSendAudios = canSendAudios, + canSendDocuments = canSendDocuments, + canSendPhotos = canSendPhotos, + canSendVideos = canSendVideos, + canSendVideoNotes = canSendVideoNotes, + canSendVoiceNotes = canSendVoiceNotes, + canSendPolls = canSendPolls, + canSendOtherMessages = canSendOtherMessages, + canAddWebPagePreviews = canAddWebPagePreviews, + canChangeInfo = canChangeInfo, + canInviteUsers = canInviteUsers, + canPinMessages = canPinMessages + ) + + /** + * Copying current instance as [ChatPermissions], but realizations of this interface may differently override this + * method + */ + fun copyCommon( + canSendPolls: Boolean? = this.canSendPolls, + canSendOtherMessages: Boolean? = this.canSendOtherMessages, + canAddWebPagePreviews: Boolean? = this.canAddWebPagePreviews, + canChangeInfo: Boolean? = this.canChangeInfo, + canInviteUsers: Boolean? = this.canInviteUsers, + canPinMessages: Boolean? = this.canPinMessages + ): ChatPermissions = ChatPermissions( + canSendMessages = null, + canSendAudios = null, + canSendDocuments = null, + canSendPhotos = null, + canSendVideos = null, + canSendVideoNotes = null, + canSendVoiceNotes = null, + canSendPolls = canSendPolls, + canSendOtherMessages = canSendOtherMessages, + canAddWebPagePreviews = canAddWebPagePreviews, + canChangeInfo = canChangeInfo, + canInviteUsers = canInviteUsers, + canPinMessages = canPinMessages + ) +} val LeftRestrictionsChatPermissions = ChatPermissions( canSendMessages = true, - canSendMediaMessages = true, + canSendAudios = true, + canSendDocuments = true, + canSendPhotos = true, + canSendVideos = true, + canSendVideoNotes = true, + canSendVoiceNotes = true, canSendPolls = true, canSendOtherMessages = true, canAddWebPagePreviews = true, @@ -37,7 +264,12 @@ val LeftRestrictionsChatPermissions = ChatPermissions( val RestrictionsChatPermissions = ChatPermissions( canSendMessages = false, - canSendMediaMessages = false, + canSendAudios = false, + canSendDocuments = false, + canSendPhotos = false, + canSendVideos = false, + canSendVideoNotes = false, + canSendVoiceNotes = false, canSendPolls = false, canSendOtherMessages = false, canAddWebPagePreviews = false, diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/chat/member/RestrictedChatMember.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/chat/member/RestrictedChatMember.kt index c3449784c8..5fb0aa985f 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/chat/member/RestrictedChatMember.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/chat/member/RestrictedChatMember.kt @@ -1,6 +1,7 @@ package dev.inmo.tgbotapi.types.chat.member import dev.inmo.tgbotapi.types.* +import dev.inmo.tgbotapi.types.chat.ChatPermissions import dev.inmo.tgbotapi.types.chat.User import kotlinx.serialization.* @@ -13,15 +14,25 @@ data class RestrictedChatMember( @SerialName(isMemberField) val isMember: Boolean = false, @SerialName(canSendMessagesField) - val canSendMessages: Boolean = false, - @SerialName(canSendMediaMessagesField) - val canSendMediaMessages: Boolean = false, + override val canSendMessages: Boolean = false, + @SerialName(canSendAudiosField) + override val canSendAudios: Boolean = false, + @SerialName(canSendDocumentsField) + override val canSendDocuments: Boolean = false, + @SerialName(canSendPhotosField) + override val canSendPhotos: Boolean = false, + @SerialName(canSendVideosField) + override val canSendVideos: Boolean = false, + @SerialName(canSendVideoNotesField) + override val canSendVideoNotes: Boolean = false, + @SerialName(canSendVoiceNotesField) + override val canSendVoiceNotes: Boolean = false, @SerialName(canSendPollsField) - val canSendPolls: Boolean = false, + override val canSendPolls: Boolean = false, @SerialName(canSendOtherMessagesField) - val canSendOtherMessages: Boolean = false, + override val canSendOtherMessages: Boolean = false, @SerialName(canAddWebPagePreviewsField) - val canAddWebpagePreviews: Boolean = false, + override val canAddWebPagePreviews: Boolean = false, @SerialName(canChangeInfoField) override val canChangeInfo: Boolean = false, @SerialName(canInviteUsersField) @@ -30,7 +41,7 @@ data class RestrictedChatMember( override val canPinMessages: Boolean = false, @SerialName(canManageTopicsField) override val canManageTopics: Boolean = false -) : BannedChatMember, SpecialRightsChatMember { +) : BannedChatMember, SpecialRightsChatMember, ChatPermissions { @SerialName(statusField) @Required private val type: String = "restricted" diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/RawMessage.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/RawMessage.kt index c5c6277daa..684b74757f 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/RawMessage.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/RawMessage.kt @@ -29,6 +29,8 @@ import dev.inmo.tgbotapi.types.passport.PassportData import dev.inmo.tgbotapi.types.payments.Invoice import dev.inmo.tgbotapi.types.payments.SuccessfulPayment import dev.inmo.tgbotapi.types.polls.Poll +import dev.inmo.tgbotapi.types.request.ChatShared +import dev.inmo.tgbotapi.types.request.UserShared import dev.inmo.tgbotapi.types.venue.Venue import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -93,6 +95,9 @@ internal data class RawMessage( private val dice: Dice? = null, private val successful_payment: SuccessfulPayment? = null, + private val user_shared: UserShared? = null, + private val chat_shared: ChatShared? = null, + // Voice Chat Service Messages private val video_chat_scheduled: VideoChatScheduled? = null, private val video_chat_started: VideoChatStarted? = null, @@ -249,6 +254,8 @@ internal data class RawMessage( successful_payment != null -> SuccessfulPaymentEvent(successful_payment) connected_website != null -> UserLoggedIn(connected_website) web_app_data != null -> web_app_data + user_shared != null -> user_shared + chat_shared != null -> chat_shared else -> null } } diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/request/ChatShared.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/request/ChatShared.kt new file mode 100644 index 0000000000..f222e5b380 --- /dev/null +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/request/ChatShared.kt @@ -0,0 +1,17 @@ +package dev.inmo.tgbotapi.types.request + +import dev.inmo.tgbotapi.types.ChatId +import dev.inmo.tgbotapi.types.ChatIdentifier +import dev.inmo.tgbotapi.types.UserId +import dev.inmo.tgbotapi.types.chatIdField +import dev.inmo.tgbotapi.types.requestIdField +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ChatShared( + @SerialName(requestIdField) + override val requestId: RequestId, + @SerialName(chatIdField) + override val chatId: ChatId +) : ChatSharedRequest diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/request/ChatSharedRequest.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/request/ChatSharedRequest.kt new file mode 100644 index 0000000000..5dbe702013 --- /dev/null +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/request/ChatSharedRequest.kt @@ -0,0 +1,8 @@ +package dev.inmo.tgbotapi.types.request + +import dev.inmo.tgbotapi.types.ChatIdentifier +import dev.inmo.tgbotapi.types.message.ChatEvents.abstracts.PrivateEvent + +sealed interface ChatSharedRequest : RequestResponse, PrivateEvent { + val chatId: ChatIdentifier +} diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/request/RequestId.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/request/RequestId.kt new file mode 100644 index 0000000000..048b7a5ff4 --- /dev/null +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/request/RequestId.kt @@ -0,0 +1,15 @@ +package dev.inmo.tgbotapi.types.request + +import kotlinx.serialization.Serializable +import kotlin.jvm.JvmInline +import kotlin.random.Random + +@Serializable +@JvmInline +value class RequestId( + val int: Int +) { + companion object { + fun random() = RequestId(Random.nextInt()) + } +} diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/request/RequestResponse.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/request/RequestResponse.kt new file mode 100644 index 0000000000..028c9be4cd --- /dev/null +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/request/RequestResponse.kt @@ -0,0 +1,8 @@ +package dev.inmo.tgbotapi.types.request + +import dev.inmo.tgbotapi.utils.internal.ClassCastsIncluded + +@ClassCastsIncluded +sealed interface RequestResponse { + val requestId: RequestId +} diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/request/UserShared.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/request/UserShared.kt new file mode 100644 index 0000000000..74b6f55667 --- /dev/null +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/request/UserShared.kt @@ -0,0 +1,19 @@ +package dev.inmo.tgbotapi.types.request + +import dev.inmo.tgbotapi.types.ChatId +import dev.inmo.tgbotapi.types.UserId +import dev.inmo.tgbotapi.types.requestIdField +import dev.inmo.tgbotapi.types.userIdField +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class UserShared( + @SerialName(requestIdField) + override val requestId: RequestId, + @SerialName(userIdField) + val userId: UserId +) : ChatSharedRequest { + override val chatId: ChatId + get() = userId +} diff --git a/tgbotapi.utils/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/utils/ClassCastsNew.kt b/tgbotapi.utils/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/utils/ClassCastsNew.kt index 500b28a94b..d6595ef0fa 100644 --- a/tgbotapi.utils/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/utils/ClassCastsNew.kt +++ b/tgbotapi.utils/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/utils/ClassCastsNew.kt @@ -113,6 +113,7 @@ import dev.inmo.tgbotapi.types.buttons.InlineKeyboardButtons.URLInlineKeyboardBu import dev.inmo.tgbotapi.types.buttons.InlineKeyboardButtons.UnknownInlineKeyboardButton import dev.inmo.tgbotapi.types.buttons.InlineKeyboardButtons.WebAppInlineKeyboardButton import dev.inmo.tgbotapi.types.buttons.InlineKeyboardMarkup +import dev.inmo.tgbotapi.types.buttons.KeyboardButtonRequestUser import dev.inmo.tgbotapi.types.buttons.KeyboardMarkup import dev.inmo.tgbotapi.types.buttons.ReplyForce import dev.inmo.tgbotapi.types.buttons.ReplyKeyboardMarkup @@ -433,6 +434,10 @@ import dev.inmo.tgbotapi.types.queries.callback.MessageCallbackQuery import dev.inmo.tgbotapi.types.queries.callback.MessageDataCallbackQuery import dev.inmo.tgbotapi.types.queries.callback.MessageGameShortNameCallbackQuery import dev.inmo.tgbotapi.types.queries.callback.UnknownCallbackQueryType +import dev.inmo.tgbotapi.types.request.ChatShared +import dev.inmo.tgbotapi.types.request.ChatSharedRequest +import dev.inmo.tgbotapi.types.request.RequestResponse +import dev.inmo.tgbotapi.types.request.UserShared import dev.inmo.tgbotapi.types.update.CallbackQueryUpdate import dev.inmo.tgbotapi.types.update.ChannelPostUpdate import dev.inmo.tgbotapi.types.update.ChatJoinRequestUpdate @@ -1879,6 +1884,34 @@ public inline fun InlineKeyboardButton.ifWebAppInlineKeyboardButton(block: (WebAppInlineKeyboardButton) -> T): T? = webAppInlineKeyboardButtonOrNull() ?.let(block) +public inline fun KeyboardButtonRequestUser.anyOrNull(): KeyboardButtonRequestUser.Any? = this as? + dev.inmo.tgbotapi.types.buttons.KeyboardButtonRequestUser.Any + +public inline fun KeyboardButtonRequestUser.anyOrThrow(): KeyboardButtonRequestUser.Any = this as + dev.inmo.tgbotapi.types.buttons.KeyboardButtonRequestUser.Any + +public inline fun KeyboardButtonRequestUser.ifAny(block: (KeyboardButtonRequestUser.Any) -> T): + T? = anyOrNull() ?.let(block) + +public inline fun KeyboardButtonRequestUser.botOrNull(): KeyboardButtonRequestUser.Bot? = this as? + dev.inmo.tgbotapi.types.buttons.KeyboardButtonRequestUser.Bot + +public inline fun KeyboardButtonRequestUser.botOrThrow(): KeyboardButtonRequestUser.Bot = this as + dev.inmo.tgbotapi.types.buttons.KeyboardButtonRequestUser.Bot + +public inline fun KeyboardButtonRequestUser.ifBot(block: (KeyboardButtonRequestUser.Bot) -> T): + T? = botOrNull() ?.let(block) + +public inline fun KeyboardButtonRequestUser.commonOrNull(): KeyboardButtonRequestUser.Common? = this + as? dev.inmo.tgbotapi.types.buttons.KeyboardButtonRequestUser.Common + +public inline fun KeyboardButtonRequestUser.commonOrThrow(): KeyboardButtonRequestUser.Common = this + as dev.inmo.tgbotapi.types.buttons.KeyboardButtonRequestUser.Common + +public inline fun + KeyboardButtonRequestUser.ifCommon(block: (KeyboardButtonRequestUser.Common) -> T): T? = + commonOrNull() ?.let(block) + public inline fun KeyboardMarkup.inlineKeyboardMarkupOrNull(): InlineKeyboardMarkup? = this as? dev.inmo.tgbotapi.types.buttons.InlineKeyboardMarkup @@ -3031,6 +3064,33 @@ public inline fun ChatEvent.successfulPaymentEventOrThrow(): SuccessfulPaymentEv public inline fun ChatEvent.ifSuccessfulPaymentEvent(block: (SuccessfulPaymentEvent) -> T): T? = successfulPaymentEventOrNull() ?.let(block) +public inline fun ChatEvent.chatSharedOrNull(): ChatShared? = this as? + dev.inmo.tgbotapi.types.request.ChatShared + +public inline fun ChatEvent.chatSharedOrThrow(): ChatShared = this as + dev.inmo.tgbotapi.types.request.ChatShared + +public inline fun ChatEvent.ifChatShared(block: (ChatShared) -> T): T? = chatSharedOrNull() + ?.let(block) + +public inline fun ChatEvent.chatSharedRequestOrNull(): ChatSharedRequest? = this as? + dev.inmo.tgbotapi.types.request.ChatSharedRequest + +public inline fun ChatEvent.chatSharedRequestOrThrow(): ChatSharedRequest = this as + dev.inmo.tgbotapi.types.request.ChatSharedRequest + +public inline fun ChatEvent.ifChatSharedRequest(block: (ChatSharedRequest) -> T): T? = + chatSharedRequestOrNull() ?.let(block) + +public inline fun ChatEvent.userSharedOrNull(): UserShared? = this as? + dev.inmo.tgbotapi.types.request.UserShared + +public inline fun ChatEvent.userSharedOrThrow(): UserShared = this as + dev.inmo.tgbotapi.types.request.UserShared + +public inline fun ChatEvent.ifUserShared(block: (UserShared) -> T): T? = userSharedOrNull() + ?.let(block) + public inline fun ForwardInfo.byAnonymousOrNull(): ForwardInfo.ByAnonymous? = this as? dev.inmo.tgbotapi.types.message.ForwardInfo.ByAnonymous @@ -4659,6 +4719,33 @@ public inline fun Poll.quizPollOrThrow(): QuizPoll = this as dev.inmo.tgbotapi.t public inline fun Poll.ifQuizPoll(block: (QuizPoll) -> T): T? = quizPollOrNull() ?.let(block) +public inline fun RequestResponse.chatSharedOrNull(): ChatShared? = this as? + dev.inmo.tgbotapi.types.request.ChatShared + +public inline fun RequestResponse.chatSharedOrThrow(): ChatShared = this as + dev.inmo.tgbotapi.types.request.ChatShared + +public inline fun RequestResponse.ifChatShared(block: (ChatShared) -> T): T? = + chatSharedOrNull() ?.let(block) + +public inline fun RequestResponse.chatSharedRequestOrNull(): ChatSharedRequest? = this as? + dev.inmo.tgbotapi.types.request.ChatSharedRequest + +public inline fun RequestResponse.chatSharedRequestOrThrow(): ChatSharedRequest = this as + dev.inmo.tgbotapi.types.request.ChatSharedRequest + +public inline fun RequestResponse.ifChatSharedRequest(block: (ChatSharedRequest) -> T): T? = + chatSharedRequestOrNull() ?.let(block) + +public inline fun RequestResponse.userSharedOrNull(): UserShared? = this as? + dev.inmo.tgbotapi.types.request.UserShared + +public inline fun RequestResponse.userSharedOrThrow(): UserShared = this as + dev.inmo.tgbotapi.types.request.UserShared + +public inline fun RequestResponse.ifUserShared(block: (UserShared) -> T): T? = + userSharedOrNull() ?.let(block) + public inline fun Update.callbackQueryUpdateOrNull(): CallbackQueryUpdate? = this as? dev.inmo.tgbotapi.types.update.CallbackQueryUpdate diff --git a/tgbotapi.utils/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/utils/types/buttons/ReplyKeyboardBuilder.kt b/tgbotapi.utils/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/utils/types/buttons/ReplyKeyboardBuilder.kt index 9bd0909aa1..22acc0e117 100644 --- a/tgbotapi.utils/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/utils/types/buttons/ReplyKeyboardBuilder.kt +++ b/tgbotapi.utils/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/utils/types/buttons/ReplyKeyboardBuilder.kt @@ -1,6 +1,10 @@ package dev.inmo.tgbotapi.extensions.utils.types.buttons import dev.inmo.tgbotapi.types.buttons.* +import dev.inmo.tgbotapi.types.buttons.reply.requestChatReplyButton +import dev.inmo.tgbotapi.types.buttons.reply.requestUserReplyButton +import dev.inmo.tgbotapi.types.chat.member.ChatAdministratorRights +import dev.inmo.tgbotapi.types.request.RequestId import dev.inmo.tgbotapi.types.webapps.WebAppInfo import dev.inmo.tgbotapi.utils.* @@ -138,3 +142,164 @@ inline fun ReplyKeyboardRowBuilder.webAppButton( text: String, url: String ) = webAppButton(text, WebAppInfo(url)) + + +/** + * Creates and put [RequestUserKeyboardButton] + * + * @see replyKeyboard + * @see ReplyKeyboardBuilder.row + */ +inline fun ReplyKeyboardRowBuilder.requestUserButton( + text: String, + requestUser: KeyboardButtonRequestUser +) = add( + requestUserReplyButton( + text, + requestUser + ) +) + +/** + * Creates and put [RequestUserKeyboardButton] with [KeyboardButtonRequestUser.Bot] + * + * @see replyKeyboard + * @see ReplyKeyboardBuilder.row + */ +inline fun ReplyKeyboardRowBuilder.requestBotButton( + text: String, + requestId: RequestId +) = requestUserButton( + text, + KeyboardButtonRequestUser.Bot(requestId) +) + +/** + * Creates and put [RequestUserKeyboardButton] with [KeyboardButtonRequestUser.Common] + * + * @see replyKeyboard + * @see ReplyKeyboardBuilder.row + */ +inline fun ReplyKeyboardRowBuilder.requestUserButton( + text: String, + requestId: RequestId, + premiumUser: Boolean? = null +) = requestUserButton( + text, + KeyboardButtonRequestUser.Common(requestId, premiumUser) +) + +/** + * Creates and put [RequestUserKeyboardButton] with [KeyboardButtonRequestUser.Any] + * + * @see replyKeyboard + * @see ReplyKeyboardBuilder.row + */ +inline fun ReplyKeyboardRowBuilder.requestUserOrBotButton( + text: String, + requestId: RequestId +) = requestUserButton( + text, + KeyboardButtonRequestUser.Any(requestId) +) + + +/** + * Creates and put [RequestChatKeyboardButton] + * + * @see replyKeyboard + * @see ReplyKeyboardBuilder.row + */ +inline fun ReplyKeyboardRowBuilder.requestChatButton( + text: String, + requestChat: KeyboardButtonRequestChat +) = add( + requestChatReplyButton( + text, + requestChat + ) +) + +/** + * Creates and put [RequestChatKeyboardButton] with [KeyboardButtonRequestChat] + * + * @see replyKeyboard + * @see ReplyKeyboardBuilder.row + */ +inline fun ReplyKeyboardRowBuilder.requestChatButton( + text: String, + requestId: RequestId, + isChannel: Boolean? = null, + isForum: Boolean? = null, + isPublic: Boolean? = null, + isOwnedBy: Boolean? = null, + userRightsInChat: ChatAdministratorRights? = null, + botRightsInChat: ChatAdministratorRights? = null, + botIsMember: Boolean? = null +) = requestChatButton( + text, + KeyboardButtonRequestChat( + requestId = requestId, + isChannel = isChannel, + isForum = isForum, + isPublic = isPublic, + isOwnedBy = isOwnedBy, + userRightsInChat = userRightsInChat, + botRightsInChat = botRightsInChat, + botIsMember = botIsMember + ) +) + +/** + * Creates and put [RequestChatKeyboardButton] with [KeyboardButtonRequestChat.Channel] + * + * @see replyKeyboard + * @see ReplyKeyboardBuilder.row + */ +inline fun ReplyKeyboardRowBuilder.requestChannelButton( + text: String, + requestId: RequestId, + isPublic: Boolean? = null, + isOwnedBy: Boolean? = null, + userRightsInChat: ChatAdministratorRights? = null, + botRightsInChat: ChatAdministratorRights? = null, + botIsMember: Boolean? = null +) = requestChatButton( + text, + KeyboardButtonRequestChat.Channel( + requestId = requestId, + isPublic = isPublic, + isOwnedBy = isOwnedBy, + userRightsInChat = userRightsInChat, + botRightsInChat = botRightsInChat, + botIsMember = botIsMember + ) +) + +/** + * Creates and put [RequestChatKeyboardButton] with [KeyboardButtonRequestChat.Group] + * + * @see replyKeyboard + * @see ReplyKeyboardBuilder.row + */ +inline fun ReplyKeyboardRowBuilder.requestGroupButton( + text: String, + requestId: RequestId, + isForum: Boolean? = null, + isPublic: Boolean? = null, + isOwnedBy: Boolean? = null, + userRightsInChat: ChatAdministratorRights? = null, + botRightsInChat: ChatAdministratorRights? = null, + botIsMember: Boolean? = null +) = requestChatButton( + text, + KeyboardButtonRequestChat.Group( + requestId = requestId, + isForum = isForum, + isPublic = isPublic, + isOwnedBy = isOwnedBy, + userRightsInChat = userRightsInChat, + botRightsInChat = botRightsInChat, + botIsMember = botIsMember + ) +) diff --git a/tgbotapi.utils/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/utils/updates/retrieving/LongPolling.kt b/tgbotapi.utils/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/utils/updates/retrieving/LongPolling.kt index 89cdc3fde6..50a592c86f 100644 --- a/tgbotapi.utils/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/utils/updates/retrieving/LongPolling.kt +++ b/tgbotapi.utils/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/utils/updates/retrieving/LongPolling.kt @@ -5,7 +5,6 @@ import dev.inmo.tgbotapi.bot.RequestsExecutor import dev.inmo.tgbotapi.bot.TelegramBot import dev.inmo.tgbotapi.bot.exceptions.* import dev.inmo.tgbotapi.extensions.utils.updates.convertWithMediaGroupUpdates -import dev.inmo.tgbotapi.extensions.utils.updates.lastUpdateIdentifier import dev.inmo.tgbotapi.requests.GetUpdates import dev.inmo.tgbotapi.requests.webhook.DeleteWebhook import dev.inmo.tgbotapi.types.* @@ -24,7 +23,8 @@ fun TelegramBot.longPollingFlow( timeoutSeconds: Seconds = 30, exceptionsHandler: (ExceptionHandler)? = null, allowedUpdates: List? = ALL_UPDATES_LIST, - autoDisableWebhooks: Boolean = true + autoDisableWebhooks: Boolean = true, + autoSkipTimeoutExceptions: Boolean = true ): Flow = channelFlow { if (autoDisableWebhooks) { runCatchingSafely { @@ -32,58 +32,77 @@ fun TelegramBot.longPollingFlow( } } + val contextSafelyExceptionHandler = coroutineContext[ContextSafelyExceptionHandlerKey] + val contextToWork = if (contextSafelyExceptionHandler == null || !autoSkipTimeoutExceptions) { + coroutineContext + } else { + coroutineContext + ContextSafelyExceptionHandler { e -> + if (e is HttpRequestTimeoutException || (e is CommonBotException && e.cause is HttpRequestTimeoutException)) { + return@ContextSafelyExceptionHandler + } else { + contextSafelyExceptionHandler.handler(e) + } + } + } + var lastUpdateIdentifier: UpdateIdentifier? = null - while (isActive) { - safely( - { e -> - exceptionsHandler ?.invoke(e) - if (e is RequestException) { - delay(1000L) + withContext(contextToWork) { + while (isActive) { + safely( + { e -> + val isHttpRequestTimeoutException = e is HttpRequestTimeoutException || (e is CommonBotException && e.cause is HttpRequestTimeoutException) + if (isHttpRequestTimeoutException && autoSkipTimeoutExceptions) { + return@safely + } + exceptionsHandler ?.invoke(e) + if (e is RequestException) { + delay(1000L) + } + if (e is GetUpdatesConflict && (exceptionsHandler == null || exceptionsHandler == defaultSafelyExceptionHandler)) { + println("Warning!!! Other bot with the same bot token requests updates with getUpdate in parallel") + } } - if (e is GetUpdatesConflict && (exceptionsHandler == null || exceptionsHandler == defaultSafelyExceptionHandler)) { - println("Warning!!! Other bot with the same bot token requests updates with getUpdate in parallel") + ) { + val updates = execute( + GetUpdates( + offset = lastUpdateIdentifier?.plus(1), + timeout = timeoutSeconds, + allowed_updates = allowedUpdates + ) + ).let { originalUpdates -> + val converted = originalUpdates.convertWithMediaGroupUpdates() + /** + * Dirty hack for cases when the media group was retrieved not fully: + * + * We are throw out the last media group and will reretrieve it again in the next get updates + * and it will guarantee that it is full + */ + /** + * Dirty hack for cases when the media group was retrieved not fully: + * + * We are throw out the last media group and will reretrieve it again in the next get updates + * and it will guarantee that it is full + */ + if ( + originalUpdates.size == getUpdatesLimit.last + && ((converted.last() as? BaseSentMessageUpdate) ?.data as? CommonMessage<*>) ?.content is MediaGroupContent<*> + ) { + converted - converted.last() + } else { + converted + } } - } - ) { - val updates = execute( - GetUpdates( - offset = lastUpdateIdentifier?.plus(1), - timeout = timeoutSeconds, - allowed_updates = allowedUpdates - ) - ).let { originalUpdates -> - val converted = originalUpdates.convertWithMediaGroupUpdates() - /** - * Dirty hack for cases when the media group was retrieved not fully: - * - * We are throw out the last media group and will reretrieve it again in the next get updates - * and it will guarantee that it is full - */ - /** - * Dirty hack for cases when the media group was retrieved not fully: - * - * We are throw out the last media group and will reretrieve it again in the next get updates - * and it will guarantee that it is full - */ - if ( - originalUpdates.size == getUpdatesLimit.last - && ((converted.last() as? BaseSentMessageUpdate) ?.data as? CommonMessage<*>) ?.content is MediaGroupContent<*> - ) { - converted - converted.last() - } else { - converted - } - } - safelyWithResult { - for (update in updates) { - send(update) + safelyWithResult { + for (update in updates) { + send(update) - lastUpdateIdentifier = update.updateId + lastUpdateIdentifier = update.updateId + } + }.onFailure { + cancel(it as? CancellationException ?: return@onFailure) } - }.onFailure { - cancel(it as? CancellationException ?: return@onFailure) } } } @@ -95,8 +114,15 @@ fun TelegramBot.startGettingOfUpdatesByLongPolling( exceptionsHandler: (ExceptionHandler)? = null, allowedUpdates: List? = ALL_UPDATES_LIST, autoDisableWebhooks: Boolean = true, + autoSkipTimeoutExceptions: Boolean = true, updatesReceiver: UpdateReceiver -): Job = longPollingFlow(timeoutSeconds, exceptionsHandler, allowedUpdates, autoDisableWebhooks).subscribeSafely( +): Job = longPollingFlow( + timeoutSeconds = timeoutSeconds, + exceptionsHandler = exceptionsHandler, + allowedUpdates = allowedUpdates, + autoDisableWebhooks = autoDisableWebhooks, + autoSkipTimeoutExceptions = autoSkipTimeoutExceptions +).subscribeSafely( scope, exceptionsHandler ?: defaultSafelyExceptionHandler, updatesReceiver @@ -111,7 +137,7 @@ fun TelegramBot.createAccumulatedUpdatesRetrieverFlow( avoidCallbackQueries: Boolean = false, exceptionsHandler: ExceptionHandler? = null, allowedUpdates: List? = ALL_UPDATES_LIST, - autoDisableWebhooks: Boolean = true, + autoDisableWebhooks: Boolean = true ): Flow = longPollingFlow( timeoutSeconds = 0, exceptionsHandler = { @@ -122,7 +148,8 @@ fun TelegramBot.createAccumulatedUpdatesRetrieverFlow( } }, allowedUpdates = allowedUpdates, - autoDisableWebhooks = autoDisableWebhooks + autoDisableWebhooks = autoDisableWebhooks, + autoSkipTimeoutExceptions = false ).filter { !(it is InlineQueryUpdate && avoidInlineQueries || it is CallbackQueryUpdate && avoidCallbackQueries) } @@ -191,9 +218,18 @@ fun TelegramBot.longPolling( timeoutSeconds: Seconds = 30, scope: CoroutineScope = CoroutineScope(Dispatchers.Default), autoDisableWebhooks: Boolean = true, + autoSkipTimeoutExceptions: Boolean = true, exceptionsHandler: ExceptionHandler? = null ): Job = updatesFilter.run { - startGettingOfUpdatesByLongPolling(timeoutSeconds, scope, exceptionsHandler, allowedUpdates, autoDisableWebhooks, asUpdateReceiver) + startGettingOfUpdatesByLongPolling( + timeoutSeconds = timeoutSeconds, + scope = scope, + exceptionsHandler = exceptionsHandler, + allowedUpdates = allowedUpdates, + autoDisableWebhooks = autoDisableWebhooks, + autoSkipTimeoutExceptions = autoSkipTimeoutExceptions, + updatesReceiver = asUpdateReceiver + ) } /** @@ -208,8 +244,9 @@ fun TelegramBot.longPolling( exceptionsHandler: ExceptionHandler? = null, flowsUpdatesFilterUpdatesKeeperCount: Int = 100, autoDisableWebhooks: Boolean = true, + autoSkipTimeoutExceptions: Boolean = true, flowUpdatesPreset: FlowsUpdatesFilter.() -> Unit -): Job = longPolling(FlowsUpdatesFilter(flowsUpdatesFilterUpdatesKeeperCount).apply(flowUpdatesPreset), timeoutSeconds, scope, autoDisableWebhooks, exceptionsHandler) +): Job = longPolling(FlowsUpdatesFilter(flowsUpdatesFilterUpdatesKeeperCount).apply(flowUpdatesPreset), timeoutSeconds, scope, autoDisableWebhooks, autoSkipTimeoutExceptions, exceptionsHandler) fun RequestsExecutor.startGettingOfUpdatesByLongPolling( updatesFilter: UpdatesFilter, @@ -217,11 +254,13 @@ fun RequestsExecutor.startGettingOfUpdatesByLongPolling( exceptionsHandler: ExceptionHandler? = null, scope: CoroutineScope = CoroutineScope(Dispatchers.Default), autoDisableWebhooks: Boolean = true, + autoSkipTimeoutExceptions: Boolean = true, ): Job = startGettingOfUpdatesByLongPolling( timeoutSeconds, scope, exceptionsHandler, updatesFilter.allowedUpdates, autoDisableWebhooks, + autoSkipTimeoutExceptions, updatesFilter.asUpdateReceiver )