diff --git a/tgbotapi.api/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/api/business/SetBusinessAccountUsernameGeneratedVariation.kt b/tgbotapi.api/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/api/business/SetBusinessAccountUsernameGeneratedVariation.kt index 9b7672edb6..36d93f38a5 100644 --- a/tgbotapi.api/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/api/business/SetBusinessAccountUsernameGeneratedVariation.kt +++ b/tgbotapi.api/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/api/business/SetBusinessAccountUsernameGeneratedVariation.kt @@ -12,5 +12,5 @@ import kotlin.String public suspend fun TelegramBot.setBusinessAccountUsername(businessConnectionId: BusinessConnectionId, username: String): Boolean = setBusinessAccountUsername( - businessConnectionId = businessConnectionId, username = with(username) { Username(username) } + businessConnectionId = businessConnectionId, username = with(username) { Username.prepare(username) } ) diff --git a/tgbotapi.core/api/tgbotapi.core.api b/tgbotapi.core/api/tgbotapi.core.api index 07a9584840..53a4e913b0 100644 --- a/tgbotapi.core/api/tgbotapi.core.api +++ b/tgbotapi.core/api/tgbotapi.core.api @@ -13436,6 +13436,7 @@ public final class dev/inmo/tgbotapi/types/Username : dev/inmo/tgbotapi/types/Ch } public final class dev/inmo/tgbotapi/types/Username$Companion { + public final fun prepare-BnpbnlE (Ljava/lang/String;)Ljava/lang/String; public final fun serializer ()Lkotlinx/serialization/KSerializer; } diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/ChatIdentifier.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/ChatIdentifier.kt index d6a969a424..48ccdd6890 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/ChatIdentifier.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/ChatIdentifier.kt @@ -106,13 +106,34 @@ fun Long.toChatId(): ChatId = ChatId(RawChatId(this)) fun Int.toChatId(): IdChatIdentifier = RawChatId(toLong()).toChatId() fun Byte.toChatId(): IdChatIdentifier = RawChatId(toLong()).toChatId() +/** + * A value class representing a username that always starts with the "@" symbol. + * + * This class is used to encapsulate the concept of a username, enforce its format, + * and ensure consistency when dealing with usernames throughout the application. + * + * @property full The full username string, guaranteed to start with "@". + * @throws IllegalArgumentException if the provided [full] value doesn't start with "@" during initialization. + */ @Serializable(ChatIdentifierSerializer::class) @JvmInline -value class Username( +value class Username ( val full: String ) : ChatIdentifier { + /** + * Retrieves the full username as a string. + * + * This property provides the complete username, which is guaranteed to start with the "@" symbol. + * It represents the raw value of the username, ensuring consistency and adherence to the required format. + */ val username: String get() = full + /** + * A property that returns the username string without the leading "@" symbol. + * + * This property removes any consecutive "@" symbols at the beginning of the `full` property + * and provides the rest of the username as a plain string. + */ val withoutAt get() = full.dropWhile { it == '@' } @@ -125,9 +146,24 @@ value class Username( override fun toString(): String { return full } + + companion object { + /** + * Prepares a valid instance of [Username] by ensuring the given string starts with "@". + * + * @param full The input string representing the username. If the string does not start with "@", + * it will be prefixed with "@". + * @return A [Username] instance constructed using the provided or modified input string. + */ + fun prepare(full: String): Username = if (full.startsWith("@")) { + Username(full) + } else { + Username("@$full") + } + } } -fun String.toUsername(): Username = Username(this) +fun String.toUsername(): Username = Username.prepare(this) @RiskFeature object ChatIdentifierSerializer : KSerializer { @@ -139,11 +175,7 @@ object ChatIdentifierSerializer : KSerializer { return id.longOrNull ?.let { ChatId(RawChatId(it)) } ?: id.content.let { - if (!it.startsWith("@")) { - Username("@$it") - } else { - Username(it) - } + Username.prepare(it) } } @@ -184,11 +216,7 @@ object FullChatIdentifierSerializer : KSerializer { else -> null } } ?: id.content.let { - if (!it.startsWith("@")) { - Username("@$it") - } else { - Username(it) - } + Username.prepare(it) } } diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/textsources/BotCommandTextSource.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/textsources/BotCommandTextSource.kt index 3b1572ee84..5277ab8c64 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/textsources/BotCommandTextSource.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/textsources/BotCommandTextSource.kt @@ -18,7 +18,7 @@ data class BotCommandTextSource @RiskFeature(DirectInvocationOfTextSourceConstru CommandRegex.find(source) ?.value ?.substring(1) ?: source.substring(1)// skip first symbol like "/" or "!" } val username: Username? by lazy { - Username(usernameRegex.find(source) ?.value ?: return@lazy null) + Username.prepare(usernameRegex.find(source) ?.value ?: return@lazy null) } override val markdown: String by lazy { source.commandMarkdown() } diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/textsources/CashTagTextSource.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/textsources/CashTagTextSource.kt index 766ee82a46..9e00c8ed77 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/textsources/CashTagTextSource.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/textsources/CashTagTextSource.kt @@ -18,7 +18,7 @@ data class CashTagTextSource @RiskFeature(DirectInvocationOfTextSourceConstructo val potentialUsername = source.dropWhile { it != '@' } if (potentialUsername.isEmpty()) return@lazy null - Username(potentialUsername) + Username.prepare(potentialUsername) } override val markdown: String by lazy { source.cashTagMarkdown() } override val markdownV2: String by lazy { cashTagMarkdownV2() } diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/textsources/HashTagTextSource.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/textsources/HashTagTextSource.kt index 0bb98715e1..1d7e585b53 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/textsources/HashTagTextSource.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/textsources/HashTagTextSource.kt @@ -18,7 +18,7 @@ data class HashTagTextSource @RiskFeature(DirectInvocationOfTextSourceConstructo val potentialUsername = source.dropWhile { it != '@' } if (potentialUsername.isEmpty()) return@lazy null - Username(potentialUsername) + Username.prepare(potentialUsername) } override val markdown: String by lazy { source.hashTagMarkdown() } diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/textsources/MentionTextSource.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/textsources/MentionTextSource.kt index 5184155442..c8d4fb1832 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/textsources/MentionTextSource.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/textsources/MentionTextSource.kt @@ -24,7 +24,7 @@ data class MentionTextSource @RiskFeature(DirectInvocationOfTextSourceConstructo override val markdown: String by lazy { source.mentionMarkdown() } override val markdownV2: String by lazy { mentionMarkdownV2() } override val html: String by lazy { mentionHTML() } - val username: Username = Username(source) + val username: Username = Username.prepare(source) init { if (!source.startsWith("@")) { diff --git a/tgbotapi.webapps/src/jsMain/kotlin/dev/inmo/tgbotapi/webapps/WebAppUser.kt b/tgbotapi.webapps/src/jsMain/kotlin/dev/inmo/tgbotapi/webapps/WebAppUser.kt index 4df597367a..cc7bc6985e 100644 --- a/tgbotapi.webapps/src/jsMain/kotlin/dev/inmo/tgbotapi/webapps/WebAppUser.kt +++ b/tgbotapi.webapps/src/jsMain/kotlin/dev/inmo/tgbotapi/webapps/WebAppUser.kt @@ -34,14 +34,14 @@ fun WebAppUser.asUser() = if (isBot == true) { id = UserId(id), firstName = firstName, lastName = lastName ?: "", - username = username ?.let(::Username) + username = username ?.let(Username::prepare) ) } else { CommonUser( id = UserId(id), firstName = firstName, lastName = lastName ?: "", - username = username ?.let(::Username), + username = username ?.let(Username::prepare), ietfLanguageCode = languageCode ?.let(::IetfLang), isPremium = isPremium )