tgbotapi/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/RawMessageEntity.kt

190 lines
7.7 KiB
Kotlin

package dev.inmo.tgbotapi.types.message
import dev.inmo.tgbotapi.types.CustomEmojiId
import dev.inmo.tgbotapi.types.chat.User
import dev.inmo.tgbotapi.types.message.textsources.*
import kotlinx.serialization.Serializable
@Serializable
internal data class RawMessageEntity(
val type: String,
val offset: Int,
val length: Int,
val url: String? = null,
val user: User? = null,
val language: String? = null,
val custom_emoji_id: CustomEmojiId? = null
) {
internal val range by lazy {
offset until (offset + length)
}
}
internal fun RawMessageEntity.asTextSource(
source: String,
subParts: List<Pair<Int, TextSource>>
): TextSource {
val sourceSubstring: String = source.substring(range)
val subPartsWithRegulars by lazy {
subParts.map { (it.first - offset) to it.second }.fillWithRegulars(sourceSubstring)
}
return when (type) {
"mention" -> MentionTextSource(sourceSubstring, subPartsWithRegulars)
"hashtag" -> HashTagTextSource(sourceSubstring, subPartsWithRegulars)
"cashtag" -> CashTagTextSource(sourceSubstring, subPartsWithRegulars)
"bot_command" -> BotCommandTextSource(sourceSubstring)
"url" -> URLTextSource(sourceSubstring)
"email" -> EMailTextSource(sourceSubstring, subPartsWithRegulars)
"phone_number" -> PhoneNumberTextSource(sourceSubstring, subPartsWithRegulars)
"bold" -> BoldTextSource(sourceSubstring, subPartsWithRegulars)
"italic" -> ItalicTextSource(sourceSubstring, subPartsWithRegulars)
"code" -> CodeTextSource(sourceSubstring)
"pre" -> PreTextSource(sourceSubstring, language)
"text_link" -> TextLinkTextSource(
sourceSubstring,
url ?: throw IllegalStateException("URL must not be null for text link")
)
"text_mention" -> TextMentionTextSource(
sourceSubstring,
user ?: throw IllegalStateException("User must not be null for text mention"),
subPartsWithRegulars
)
"underline" -> UnderlineTextSource(sourceSubstring, subPartsWithRegulars)
"strikethrough" -> StrikethroughTextSource(sourceSubstring, subPartsWithRegulars)
"spoiler" -> SpoilerTextSource(sourceSubstring, subPartsWithRegulars)
"custom_emoji" -> CustomEmojiTextSource(sourceSubstring, custom_emoji_id ?: error("For custom emoji custom_emoji_id should exists"), subPartsWithRegulars)
else -> RegularTextSource(sourceSubstring)
}
}
private inline operator fun <T : Comparable<T>> ClosedRange<T>.contains(other: ClosedRange<T>): Boolean {
return start <= other.start && endInclusive >= other.endInclusive
}
internal fun List<Pair<Int, TextSource>>.fillWithRegulars(source: String): TextSourcesList {
var index = 0
val result = mutableListOf<TextSource>()
for (i in indices) {
val (offset, textSource) = get(i)
if (offset - index > 0) {
result.add(regular(source.substring(index, offset)))
index = offset
}
result.add(textSource)
index += textSource.source.length
}
if (index != source.length) {
result.add(regular(source.substring(index, source.length)))
}
return result
}
private fun createTextSources(
originalFullString: String,
entities: RawMessageEntities
): List<Pair<Int, TextSource>> {
val mutableEntities = entities.toMutableList().apply { sortBy { it.offset } }
val resultList = mutableListOf<Pair<Int, TextSource>>()
while (mutableEntities.isNotEmpty()) {
var parent = mutableEntities.removeFirst()
val subentities = mutableListOf<RawMessageEntity>()
val toAddCutted = mutableListOf<RawMessageEntity>()
while (mutableEntities.isNotEmpty()) {
val potentialParent = mutableEntities.first()
when {
potentialParent.range.first > parent.range.last -> break
potentialParent.range in parent.range -> {
subentities.add(potentialParent)
}
potentialParent.offset == parent.offset && potentialParent.length > parent.length -> {
subentities.add(parent)
parent = potentialParent
}
else -> { // need to cut
toAddCutted.add(potentialParent)
}
}
mutableEntities.remove(potentialParent)
}
val subtextSources = if (subentities.isNotEmpty()) {
mutableEntities.removeAll(subentities)
if (toAddCutted.isNotEmpty()) {
val borderIndex = parent.range.last + 1
mutableEntities.addAll(
0,
toAddCutted.map {
val firstLength = borderIndex - it.offset
subentities.add(it.copy(length = firstLength))
it.copy(
offset = borderIndex,
length = it.length - firstLength
)
}
)
}
createTextSources(originalFullString, subentities)
} else {
emptyList()
}
resultList.add(
parent.offset to parent.asTextSource(
originalFullString,
subtextSources
)
)
}
return resultList
}
internal fun TextSource.toRawMessageEntities(offset: Int = 0): List<RawMessageEntity> {
val source = source
val length = source.length
return listOfNotNull(
when (this) {
is MentionTextSource -> RawMessageEntity("mention", offset, length)
is HashTagTextSource -> RawMessageEntity("hashtag", offset, length)
is CashTagTextSource -> RawMessageEntity("cashtag", offset, length)
is BotCommandTextSource -> RawMessageEntity("bot_command", offset, length)
is URLTextSource -> RawMessageEntity("url", offset, length)
is EMailTextSource -> RawMessageEntity("email", offset, length)
is PhoneNumberTextSource -> RawMessageEntity("phone_number", offset, length)
is BoldTextSource -> RawMessageEntity("bold", offset, length)
is ItalicTextSource -> RawMessageEntity("italic", offset, length)
is CodeTextSource -> RawMessageEntity("code", offset, length)
is PreTextSource -> RawMessageEntity("pre", offset, length, language = language)
is TextLinkTextSource -> RawMessageEntity("text_link", offset, length, url)
is TextMentionTextSource -> RawMessageEntity("text_mention", offset, length, user = user)
is UnderlineTextSource -> RawMessageEntity("underline", offset, length)
is StrikethroughTextSource -> RawMessageEntity("strikethrough", offset, length)
is SpoilerTextSource -> RawMessageEntity("spoiler", offset, length)
is CustomEmojiTextSource -> RawMessageEntity("custom_emoji", offset, length, custom_emoji_id = customEmojiId)
is RegularTextSource -> null
}
) + if (this is MultilevelTextSource) {
subsources.toRawMessageEntities(offset)
} else {
emptyList()
}
}
internal fun TextSourcesList.toRawMessageEntities(preOffset: Int = 0): List<RawMessageEntity> {
var i = preOffset
return flatMap { textSource ->
textSource.toRawMessageEntities(i).also {
i += it.maxByOrNull { it.length }?.length ?: textSource.source.length
}
}
}
internal fun TextSourcesList.toRawMessageEntities(): List<RawMessageEntity> = toRawMessageEntities(0)
internal fun RawMessageEntities.asTextSources(sourceString: String): TextSourcesList =
createTextSources(sourceString, this).fillWithRegulars(sourceString)
internal typealias RawMessageEntities = List<RawMessageEntity>