From 3fb80dd475a4192441bb0ae37053f0aa25c8dcdd Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Fri, 24 Apr 2020 17:51:09 +0600 Subject: [PATCH] add support of income explanation functionality in polls and polls auto close functionality --- CHANGELOG.md | 16 ++ .../TelegramBotAPI/types/Common.kt | 5 + .../types/MessageEntity/RawMessageEntity.kt | 22 ++ .../textsources/TextLinkTextSource.kt | 2 +- .../textsources/TextMentionTextSource.kt | 9 +- .../TelegramBotAPI/types/polls/Poll.kt | 210 +++++++++++++----- 6 files changed, 206 insertions(+), 58 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1895365ff9..5c83f4dbad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,22 @@ * Versions: * `Kotlin`: `1.3.71` -> `1.3.72` * `Klock`: `1.10.3` -> `1.10.5` +* `TelegramBotAPI`: + * Typealias `LongSeconds` was added for correct explanation of seconds in `Long` primitive type + * Several new fields was added: + * `explanationField` + * `explanationEntitiesField` + * `openPeriodField` + * `closeDateField` + * Field `TextLinkTextSource#url` was added + * Field `TextMentionTextSource#user` was added + * Sealed class `ScheduledCloseInfo` was added + * Class `ExactScheduledCloseInfo` was added for cases with `close_date` + * Class `ApproximateScheduledCloseInfo` was added for cases with `open_period` + * Field `Poll#scheduledCloseInfo` was added + * Sealed class `MultipleAnswersPoll` was added + * Class `RegularPoll` now extends `MultipleAnswersPoll` + ## 0.26.0 diff --git a/TelegramBotAPI/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/types/Common.kt b/TelegramBotAPI/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/types/Common.kt index 3061fec7b9..cf593a0482 100644 --- a/TelegramBotAPI/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/types/Common.kt +++ b/TelegramBotAPI/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/types/Common.kt @@ -22,6 +22,7 @@ typealias FileUniqueId = String typealias DiceResult = Int typealias Seconds = Int +typealias LongSeconds = Long val getUpdatesLimit = 1 .. 100 val callbackQueryAnswerLength = 0 until 200 @@ -199,6 +200,7 @@ const val tgsStickerField = "tgs_sticker" const val okField = "ok" const val captionField = "caption" +const val explanationField = "explanation" const val idField = "id" const val pollIdField = "poll_id" const val textField = "text" @@ -250,6 +252,9 @@ const val xShiftField = "x_shift" const val yShiftField = "y_shift" const val scaleField = "scale" +const val explanationEntitiesField = "explanation_entities" +const val openPeriodField = "open_period" +const val closeDateField = "close_date" const val smallFileIdField = "small_file_id" const val bigFileIdField = "big_file_id" diff --git a/TelegramBotAPI/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/types/MessageEntity/RawMessageEntity.kt b/TelegramBotAPI/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/types/MessageEntity/RawMessageEntity.kt index c7023471e4..6a2862a2fa 100644 --- a/TelegramBotAPI/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/types/MessageEntity/RawMessageEntity.kt +++ b/TelegramBotAPI/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/types/MessageEntity/RawMessageEntity.kt @@ -83,6 +83,28 @@ internal fun createTextPart(from: String, entities: RawMessageEntities): List.asRawMessageEntities() = mapNotNull { + val source = it.source + when (source) { + is MentionTextSource -> RawMessageEntity("mention", it.range.first, it.range.last - it.range.first) + is HashTagTextSource -> RawMessageEntity("hashtag", it.range.first, it.range.last - it.range.first) + is CashTagTextSource -> RawMessageEntity("cashtag", it.range.first, it.range.last - it.range.first) + is BotCommandTextSource -> RawMessageEntity("bot_command", it.range.first, it.range.last - it.range.first) + is URLTextSource -> RawMessageEntity("url", it.range.first, it.range.last - it.range.first) + is EMailTextSource -> RawMessageEntity("email", it.range.first, it.range.last - it.range.first) + is PhoneNumberTextSource -> RawMessageEntity("phone_number", it.range.first, it.range.last - it.range.first) + is BoldTextSource -> RawMessageEntity("bold", it.range.first, it.range.last - it.range.first) + is ItalicTextSource -> RawMessageEntity("italic", it.range.first, it.range.last - it.range.first) + is CodeTextSource -> RawMessageEntity("code", it.range.first, it.range.last - it.range.first) + is PreTextSource -> RawMessageEntity("pre", it.range.first, it.range.last - it.range.first, language = source.language) + is TextLinkTextSource -> RawMessageEntity("text_link", it.range.first, it.range.last - it.range.first, source.url) + is TextMentionTextSource -> RawMessageEntity("text_mention", it.range.first, it.range.last - it.range.first, user = source.user) + is UnderlineTextSource -> RawMessageEntity("underline", it.range.first, it.range.last - it.range.first) + is StrikethroughTextSource -> RawMessageEntity("strikethrough", it.range.first, it.range.last - it.range.first) + else -> null + } +} + internal fun RawMessageEntities.asTextParts(sourceString: String): List = createTextPart(sourceString, this) internal typealias RawMessageEntities = List diff --git a/TelegramBotAPI/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/types/MessageEntity/textsources/TextLinkTextSource.kt b/TelegramBotAPI/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/types/MessageEntity/textsources/TextLinkTextSource.kt index a4ba378bad..0b89463c32 100644 --- a/TelegramBotAPI/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/types/MessageEntity/textsources/TextLinkTextSource.kt +++ b/TelegramBotAPI/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/types/MessageEntity/textsources/TextLinkTextSource.kt @@ -5,7 +5,7 @@ import com.github.insanusmokrassar.TelegramBotAPI.utils.* class TextLinkTextSource( override val source: String, - url: String + val url: String ) : TextSource { override val asMarkdownSource: String by lazy { source.linkMarkdown(url) } override val asMarkdownV2Source: String by lazy { source.linkMarkdownV2(url) } diff --git a/TelegramBotAPI/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/types/MessageEntity/textsources/TextMentionTextSource.kt b/TelegramBotAPI/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/types/MessageEntity/textsources/TextMentionTextSource.kt index 061b5f1fb8..8f1a37fd4f 100644 --- a/TelegramBotAPI/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/types/MessageEntity/textsources/TextMentionTextSource.kt +++ b/TelegramBotAPI/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/types/MessageEntity/textsources/TextMentionTextSource.kt @@ -2,16 +2,17 @@ package com.github.insanusmokrassar.TelegramBotAPI.types.MessageEntity.textsourc import com.github.insanusmokrassar.TelegramBotAPI.CommonAbstracts.MultilevelTextSource import com.github.insanusmokrassar.TelegramBotAPI.CommonAbstracts.TextPart +import com.github.insanusmokrassar.TelegramBotAPI.types.User import com.github.insanusmokrassar.TelegramBotAPI.types.chat.abstracts.PrivateChat import com.github.insanusmokrassar.TelegramBotAPI.utils.* class TextMentionTextSource( override val source: String, - privateChat: PrivateChat, + val user: User, textParts: List ) : MultilevelTextSource { override val textParts: List by lazy { source.fullListOfSubSource(textParts) } - override val asMarkdownSource: String by lazy { source.textMentionMarkdown(privateChat.id) } - override val asMarkdownV2Source: String by lazy { textMentionMarkdownV2(privateChat.id) } - override val asHtmlSource: String by lazy { textMentionHTML(privateChat.id) } + override val asMarkdownSource: String by lazy { source.textMentionMarkdown(user.id) } + override val asMarkdownV2Source: String by lazy { textMentionMarkdownV2(user.id) } + override val asHtmlSource: String by lazy { textMentionHTML(user.id) } } diff --git a/TelegramBotAPI/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/types/polls/Poll.kt b/TelegramBotAPI/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/types/polls/Poll.kt index 2c9990e5b5..90cb875245 100644 --- a/TelegramBotAPI/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/types/polls/Poll.kt +++ b/TelegramBotAPI/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/types/polls/Poll.kt @@ -1,11 +1,33 @@ package com.github.insanusmokrassar.TelegramBotAPI.types.polls +import com.github.insanusmokrassar.TelegramBotAPI.CommonAbstracts.CaptionedInput +import com.github.insanusmokrassar.TelegramBotAPI.CommonAbstracts.TextPart import com.github.insanusmokrassar.TelegramBotAPI.types.* +import com.github.insanusmokrassar.TelegramBotAPI.types.MessageEntity.* +import com.github.insanusmokrassar.TelegramBotAPI.types.MessageEntity.RawMessageEntity +import com.github.insanusmokrassar.TelegramBotAPI.types.MessageEntity.asTextParts import com.github.insanusmokrassar.TelegramBotAPI.utils.nonstrictJsonFormat +import com.soywiz.klock.DateTime +import com.soywiz.klock.TimeSpan import kotlinx.serialization.* -import kotlinx.serialization.builtins.ListSerializer import kotlinx.serialization.json.* +sealed class ScheduledCloseInfo { + abstract val closeDateTime: DateTime +} + +data class ExactScheduledCloseInfo( + override val closeDateTime: DateTime +) : ScheduledCloseInfo() + +data class ApproximateScheduledCloseInfo( + val openDuration: TimeSpan +) : ScheduledCloseInfo() { + @Suppress("MemberVisibilityCanBePrivate") + val startPoint = DateTime.now() + override val closeDateTime: DateTime = startPoint + openDuration +} + @Serializable(PollSerializer::class) sealed class Poll { abstract val id: PollIdentifier @@ -14,6 +36,51 @@ sealed class Poll { abstract val votesCount: Int abstract val isClosed: Boolean abstract val isAnonymous: Boolean + abstract val scheduledCloseInfo: ScheduledCloseInfo? +} + +@Serializable(PollSerializer::class) +sealed class MultipleAnswersPoll : Poll() + +@Serializable +private class RawPoll( + @SerialName(idField) + val id: PollIdentifier, + @SerialName(questionField) + val question: String, + @SerialName(optionsField) + val options: List, + @SerialName(totalVoterCountField) + val votesCount: Int, + @SerialName(isClosedField) + val isClosed: Boolean = false, + @SerialName(isAnonymousField) + val isAnonymous: Boolean = false, + @SerialName(typeField) + val type: String, + @SerialName(allowsMultipleAnswersField) + val allowMultipleAnswers: Boolean = false, + @SerialName(correctOptionIdField) + val correctOptionId: Int? = null, + @SerialName(explanationField) + val caption: String? = null, + @SerialName(explanationEntitiesField) + val captionEntities: List = emptyList(), + @SerialName(openPeriodField) + val openPeriod: LongSeconds? = null, + @SerialName(closeDateField) + val closeDate: LongSeconds? = null +) { + @Transient + val scheduledCloseInfo: ScheduledCloseInfo? = closeDate ?.let { + ExactScheduledCloseInfo( + DateTime(unixMillis = it * 1000.0) + ) + } ?: openPeriod ?.let { + ApproximateScheduledCloseInfo( + TimeSpan(it * 1000.0) + ) + } } @Serializable @@ -30,91 +97,128 @@ data class UnknownPollType internal constructor( override val isClosed: Boolean = false, @SerialName(isAnonymousField) override val isAnonymous: Boolean = false, - val raw: String -) : Poll() + @Serializable + val raw: JsonObject +) : Poll() { + @Transient + override val scheduledCloseInfo: ScheduledCloseInfo? = raw.getPrimitiveOrNull(closeDateField) ?.longOrNull ?.let { + ExactScheduledCloseInfo( + DateTime(unixMillis = it * 1000.0) + ) + } ?: raw.getPrimitiveOrNull(durationField) ?.longOrNull ?.let { + ApproximateScheduledCloseInfo( + TimeSpan(it * 1000.0) + ) + } +} -@Serializable +@Serializable(PollSerializer::class) data class RegularPoll( - @SerialName(idField) override val id: PollIdentifier, - @SerialName(questionField) override val question: String, - @SerialName(optionsField) override val options: List, - @SerialName(totalVoterCountField) override val votesCount: Int, - @SerialName(isClosedField) override val isClosed: Boolean = false, - @SerialName(isAnonymousField) override val isAnonymous: Boolean = false, - @SerialName(allowsMultipleAnswersField) - val allowMultipleAnswers: Boolean = false -) : Poll() + val allowMultipleAnswers: Boolean = false, + override val scheduledCloseInfo: ScheduledCloseInfo? = null +) : MultipleAnswersPoll() -@Serializable +@Serializable(PollSerializer::class) data class QuizPoll( - @SerialName(idField) override val id: PollIdentifier, - @SerialName(questionField) override val question: String, - @SerialName(optionsField) override val options: List, - @SerialName(totalVoterCountField) override val votesCount: Int, /** * Nullable due to documentation (https://core.telegram.org/bots/api#poll) */ - @SerialName(correctOptionIdField) val correctOptionId: Int? = null, - @SerialName(isClosedField) + override val caption: String? = null, + override val captionEntities: List = emptyList(), override val isClosed: Boolean = false, - @SerialName(isAnonymousField) - override val isAnonymous: Boolean = false -) : Poll() + override val isAnonymous: Boolean = false, + override val scheduledCloseInfo: ScheduledCloseInfo? = null +) : Poll(), CaptionedInput @Serializer(Poll::class) internal object PollSerializer : KSerializer { - private val pollOptionsSerializer = ListSerializer(PollOption.serializer()) + override val descriptor: SerialDescriptor + get() = RawPoll.serializer().descriptor + override fun deserialize(decoder: Decoder): Poll { val asJson = JsonObjectSerializer.deserialize(decoder) + val rawPoll = nonstrictJsonFormat.fromJson(RawPoll.serializer(), asJson) - return when (asJson.getPrimitive(typeField).content) { - regularPollType -> nonstrictJsonFormat.fromJson( - RegularPoll.serializer(), - asJson + return when (rawPoll.type) { + quizPollType -> QuizPoll( + rawPoll.id, + rawPoll.question, + rawPoll.options, + rawPoll.votesCount, + rawPoll.correctOptionId, + rawPoll.caption, + rawPoll.caption?.let { rawPoll.captionEntities.asTextParts(it) } ?: emptyList(), + rawPoll.isClosed, + rawPoll.isAnonymous, + rawPoll.scheduledCloseInfo ) - quizPollType -> nonstrictJsonFormat.fromJson( - QuizPoll.serializer(), - asJson + regularPollType -> RegularPoll( + rawPoll.id, + rawPoll.question, + rawPoll.options, + rawPoll.votesCount, + rawPoll.isClosed, + rawPoll.isAnonymous, + rawPoll.allowMultipleAnswers, + rawPoll.scheduledCloseInfo ) else -> UnknownPollType( - asJson.getPrimitive(idField).content, - asJson.getPrimitive(questionField).content, - nonstrictJsonFormat.fromJson( - pollOptionsSerializer, - asJson.getArray(optionsField) - ), - asJson.getPrimitive(totalVoterCountField).int, - asJson.getPrimitiveOrNull(isClosedField) ?.booleanOrNull ?: false, - asJson.getPrimitiveOrNull(isAnonymousField) ?.booleanOrNull ?: true, - asJson.toString() + rawPoll.id, + rawPoll.question, + rawPoll.options, + rawPoll.votesCount, + rawPoll.isClosed, + rawPoll.isAnonymous, + asJson ) } } override fun serialize(encoder: Encoder, value: Poll) { - val asJson = when (value) { - is RegularPoll -> nonstrictJsonFormat.toJson(RegularPoll.serializer(), value) - is QuizPoll -> nonstrictJsonFormat.toJson(QuizPoll.serializer(), value) - is UnknownPollType -> throw IllegalArgumentException("Currently unable to correctly serialize object of poll $value") + val closeInfo = value.scheduledCloseInfo + val rawPoll = when (value) { + is RegularPoll -> RawPoll( + value.id, + value.question, + value.options, + value.votesCount, + value.isClosed, + value.isAnonymous, + regularPollType, + value.allowMultipleAnswers, + openPeriod = (closeInfo as? ApproximateScheduledCloseInfo) ?.openDuration ?.seconds ?.toLong(), + closeDate = (closeInfo as? ExactScheduledCloseInfo) ?.closeDateTime ?.unixMillisLong ?.div(1000L) + ) + is QuizPoll -> RawPoll( + value.id, + value.question, + value.options, + value.votesCount, + value.isClosed, + value.isAnonymous, + regularPollType, + correctOptionId = value.correctOptionId, + caption = value.caption, + captionEntities = value.captionEntities.asRawMessageEntities(), + openPeriod = (closeInfo as? ApproximateScheduledCloseInfo) ?.openDuration ?.seconds ?.toLong(), + closeDate = (closeInfo as? ExactScheduledCloseInfo) ?.closeDateTime ?.unixMillisLong ?.div(1000L) + ) + is UnknownPollType -> { + JsonObjectSerializer.serialize(encoder, value.raw) + return + } } - val resultJson = JsonObject( - asJson.jsonObject + (typeField to when (value) { - is RegularPoll -> JsonPrimitive(regularPollType) - is QuizPoll -> JsonPrimitive(quizPollType) - is UnknownPollType -> throw IllegalArgumentException("Currently unable to correctly serialize object of poll $value") - }) - ) - JsonObjectSerializer.serialize(encoder, resultJson) + RawPoll.serializer().serialize(encoder, rawPoll) } }