Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate to Bot API v7.2 #5

Merged
merged 2 commits into from
May 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3,089 changes: 2,429 additions & 660 deletions api-spec/telegram-bot-api.html

Large diffs are not rendered by default.

3,298 changes: 2,515 additions & 783 deletions api/tbot-api.api

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,20 @@ import me.alllex.parsus.token.regexToken
import org.jsoup.Jsoup
import org.jsoup.nodes.Element

private val typeSubstitutions = mapOf(
"MessageId" to "MessageIdResult"

/**
* Just use a different name for a type, because it clashes with something else
*/
private val typeRename = mapOf(
"MessageId" to "MessageRef",
)

/**
* Replace the original types with another type and do not generate a data class for the original type
*/
private val typeSubstitution = mapOf(
"InaccessibleMessage" to "Message",
"MaybeInaccessibleMessage" to "Message",
)

private val fieldTypeSubstitutionsByFieldName = mapOf(
Expand All @@ -21,7 +33,7 @@ private val fieldTypeSubstitutionsByFieldName = mapOf(
)

fun resolveElementTypeName(name: String): String {
return typeSubstitutions[name] ?: name
return typeSubstitution[name] ?: typeRename[name] ?: name
}

fun resolveFieldTypeName(serialFieldName: String, serialFieldType: String): String {
Expand Down Expand Up @@ -63,7 +75,8 @@ data class BotApiElement(
val name: BotApiElementName,
val description: String,
val fields: List<Field>? = null,
val unionTypes: List<BotApiElementName>? = null
val unionTypes: List<BotApiElementName>? = null,
val originalName: BotApiElementName = name,
) {
data class Field(
val serialName: String,
Expand Down Expand Up @@ -94,6 +107,8 @@ val availableTypesIgnoredSections: Set<String> = setOf(
"Inline mode objects",
"Determining list of commands",
"Formatting options",
"Accent colors",
"Profile accent colors",
"Inline mode methods",
)

Expand All @@ -114,8 +129,9 @@ val responseTypeRegexes = listOf(
Regex("Returns (\\w+) on success"),
Regex("[Rr]eturns an? (.+?) object"),
Regex("in form of a (\\w+) object"),
Regex("[Oo]n success, an (.+?) of the sent messages is returned"),
Regex("[Oo]n success, (\\w+) is returned"),
Regex("the sent (\\w+) is returned"),
Regex("[Oo]n success, the sent (\\w+) is returned"),
Regex("[Oo]n success, an? (.+) objects?"),
Regex("On success, an (.+) that were"),
Regex("On success, the stopped (\\w+) with the final results is returned"),
Expand Down Expand Up @@ -160,7 +176,8 @@ class BotApiDefinitionParser {

val contentEls = devPageContent.children()

val h3Sections = contentEls.selectSections(startsSection = Element::isH3, stopsSequence = { it.isH1() || it.isH2() })
val h3Sections =
contentEls.selectSections(startsSection = Element::isH3, stopsSequence = { it.isH1() || it.isH2() })

val groupSections = h3Sections.filter { it.first().ownText() in topLevelSections }
println("Found ${groupSections.size} top-level sections")
Expand All @@ -176,8 +193,12 @@ class BotApiDefinitionParser {
println("Found ${methodDefSections.size} method definition sections")

val typeDefinitions = typeDefSections.map(::parseElementDefinition)
val types = typeDefinitions.map(this::parseElement) +
listOf(implicitReplyMarkupElement)

@Suppress("RemoveExplicitTypeArguments")
val types = buildList<BotApiElement> {
this += typeDefinitions.map(::parseElement).filter { it.originalName.value !in typeSubstitution }
this += implicitReplyMarkupElement
}

val methodDefinitions = methodDefSections.map(::parseMethodDefinition)
val methods = methodDefinitions.map(this::parseMethod)
Expand Down Expand Up @@ -233,7 +254,7 @@ class BotApiDefinitionParser {
?.sortedBy { it.isOptional }
val unionTypes = elementDef.unionTypesListElement?.let { parseUnionTypes(it) }
val finalFields = if (unionTypes == null && fields == null) emptyList() else fields
return BotApiElement(name, description, finalFields, unionTypes)
return BotApiElement(name, description, finalFields, unionTypes, BotApiElementName(initialName))
}

private fun parseMethod(methodDef: ApiMethodDefinition): BotApiMethod {
Expand All @@ -251,7 +272,10 @@ class BotApiDefinitionParser {
val typeFieldInfo = try {
serialTypeToKotlinTypeString(returnTypeText, isOptional = false)
} catch (e: Exception) {
throw RuntimeException("Failed to parse return type from '$returnTypeText' in description:\n---\n$methodDescriptionText\n---\n", e)
throw RuntimeException(
"Failed to parse return type from '$returnTypeText' in description:\n---\n$methodDescriptionText\n---\n",
e
)
}
return typeFieldInfo.kotlinType
}
Expand Down Expand Up @@ -293,13 +317,21 @@ class BotApiDefinitionParser {
val nameEl = row.child(0)
val typeEl = row.child(1)
val descriptionEl = row.child(2)
val isOptional = descriptionEl.text().startsWith("Optional")
val description = descriptionEl.text().trim() // TODO: replace <img ... alt="😐"> with actual emoji text
val isOptional = description.startsWith("Optional")
val serialFieldName = nameEl.text().trim()
val typeText = typeEl.text().trim()
val typeFieldInfo = serialTypeToKotlinTypeString(typeText, isOptional, serialFieldName)
val description = descriptionEl.text().trim()

fields.add(BotApiElement.Field(serialFieldName, description, typeFieldInfo.kotlinType, isOptional, typeFieldInfo.defaultValue))
fields.add(
BotApiElement.Field(
serialFieldName,
description,
typeFieldInfo.kotlinType,
isOptional,
typeFieldInfo.defaultValue
)
)
}

return fields
Expand Down Expand Up @@ -334,15 +366,27 @@ class BotApiDefinitionParser {
val typeFieldInfo = serialTypeToKotlinTypeString(typeText, isOptional, serialFieldName)
val description = descriptionEl.text().trim()

fields.add(BotApiElement.Field(serialFieldName, description, typeFieldInfo.kotlinType, isOptional, typeFieldInfo.defaultValue))
fields.add(
BotApiElement.Field(
serialFieldName,
description,
typeFieldInfo.kotlinType,
isOptional,
typeFieldInfo.defaultValue
)
)
}

return fields
}

private data class ResolvedTypeInfo(val kotlinType: KotlinType, val defaultValue: String?)

private fun serialTypeToKotlinTypeString(serialType: String, isOptional: Boolean, serialFieldName: String? = null): ResolvedTypeInfo {
private fun serialTypeToKotlinTypeString(
serialType: String,
isOptional: Boolean,
serialFieldName: String? = null
): ResolvedTypeInfo {
val result = FieldTypeGrammar.parse(serialType)

val parsedType = result.getOrElse {
Expand Down Expand Up @@ -371,7 +415,7 @@ object FieldTypeGrammar : Grammar<String>(ignoreCase = true) {
val message by regexToken("Messages") map { "Message" } // typo in the original HTML spec
val replayMarkup by regexToken("InlineKeyboardMarkup or ReplyKeyboardMarkup or ReplyKeyboardRemove or ForceReply") map { "ReplyMarkup" } // this is a custom sealed type
val inputMedia by regexToken("InputMediaAudio, InputMediaDocument, InputMediaPhoto and InputMediaVideo") map { "InputMedia" }
val apiType by regexToken("\\w+") map { typeSubstitutions[it.text] ?: it.text }
val apiType by regexToken("\\w+") map { resolveElementTypeName(it.text) }

val simpleType by int or double or boolean or string or message or replayMarkup or inputMedia or apiType
val listType by parser {
Expand All @@ -385,7 +429,10 @@ object FieldTypeGrammar : Grammar<String>(ignoreCase = true) {
override val root by fieldType
}

private fun <T> List<T>.selectSections(startsSection: (T) -> Boolean, stopsSequence: (T) -> Boolean = { false }): List<List<T>> {
private fun <T> List<T>.selectSections(
startsSection: (T) -> Boolean,
stopsSequence: (T) -> Boolean = { false }
): List<List<T>> {
val result = mutableListOf<MutableList<T>>()
var subList: MutableList<T>? = null

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ val valueTypes = listOf(

private val unionMarkerValueInDescriptionRe = Regex("""(?:must be|always) ["“”]?([\w_]+)["“”]?""")

private val unionMarkerFieldNames = setOf("type", "status")
private val unionMarkerFieldNames = setOf("type", "status", "source")

fun BotApiElement.Field.isUnionDiscriminator(): Boolean {
return serialName in unionMarkerFieldNames
Expand Down Expand Up @@ -218,18 +218,18 @@ val fluentMethods = listOf(
FluentContextMethod("ChatId", "sendMarkdownV2", "sendMessage", mapOf("chatId" to "this", "parseMode" to "ParseMode.MARKDOWN_V2")),
FluentContextMethod("ChatId", "sendHtml", "sendMessage", mapOf("chatId" to "this", "parseMode" to "ParseMode.HTML")),

FluentContextMethod("Message", "reply", "sendMessage", mapOf("chatId" to "chat.id", "replyToMessageId" to "messageId")),
FluentContextMethod("Message", "reply", "sendMessage", mapOf("chatId" to "chat.id", "replyParameters" to "ReplyParameters(messageId)")),
FluentContextMethod(
"Message", "replyMarkdown",
"sendMessage", mapOf("chatId" to "chat.id", "replyToMessageId" to "messageId", "parseMode" to "ParseMode.MARKDOWN")
"sendMessage", mapOf("chatId" to "chat.id", "replyParameters" to "ReplyParameters(messageId)", "parseMode" to "ParseMode.MARKDOWN")
),
FluentContextMethod(
"Message", "replyMarkdownV2",
"sendMessage", mapOf("chatId" to "chat.id", "replyToMessageId" to "messageId", "parseMode" to "ParseMode.MARKDOWN_V2")
"sendMessage", mapOf("chatId" to "chat.id", "replyParameters" to "ReplyParameters(messageId)", "parseMode" to "ParseMode.MARKDOWN_V2")
),
FluentContextMethod(
"Message", "replyHtml",
"sendMessage", mapOf("chatId" to "chat.id", "replyToMessageId" to "messageId", "parseMode" to "ParseMode.HTML")
"sendMessage", mapOf("chatId" to "chat.id", "replyParameters" to "ReplyParameters(messageId)", "parseMode" to "ParseMode.HTML")
),

FluentContextMethod(
Expand Down Expand Up @@ -781,6 +781,7 @@ class BotApiGenerator {

appendLine("sealed interface ${name.value}")

// TODO: add value-based discrimination
if (discriminatorFieldName == null) {
val avoidFields = setOf("description")
val discriminatorFieldByType = unionTypes.associate { unionType ->
Expand Down
Loading
Loading