mirror of
https://github.com/amalthea-mc/spigot-tg-bridge.git
synced 2024-11-23 10:52:26 +00:00
Move to kotlinx coroutines
This commit is contained in:
parent
443dfb9394
commit
41fbe85f7f
@ -36,7 +36,8 @@ repositories {
|
||||
}
|
||||
|
||||
val tgBotVersion = "6.0.4"
|
||||
val plugDir = "MinecraftServers/spigot_1.16.5/plugins/"
|
||||
val retrofitVersion = "2.7.1"
|
||||
val plugDir = "MinecraftServers/spigot_1.17/plugins/"
|
||||
val homeDir = System.getProperty("user.home")
|
||||
|
||||
tasks {
|
||||
@ -45,6 +46,9 @@ tasks {
|
||||
"spigot-tg-bridge-${spigotApiVersion}-v${pluginVersion}.jar"
|
||||
)
|
||||
}
|
||||
build {
|
||||
dependsOn("shadowJar")
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register<Copy>("copyArtifacts") {
|
||||
@ -57,15 +61,14 @@ tasks.register("pack") {
|
||||
finalizedBy("copyArtifacts")
|
||||
}
|
||||
|
||||
defaultTasks("pack")
|
||||
|
||||
dependencies {
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
|
||||
compileOnly("org.spigotmc:spigot-api:$spigotApiVersion-R0.1-SNAPSHOT")
|
||||
implementation(
|
||||
"io.github.kotlin-telegram-bot.kotlin-telegram-bot" +
|
||||
":telegram:$tgBotVersion"
|
||||
)
|
||||
implementation("com.google.code.gson:gson:2.8.7")
|
||||
implementation("com.squareup.retrofit2:retrofit:$retrofitVersion")
|
||||
implementation("com.squareup.retrofit2:converter-gson:$retrofitVersion")
|
||||
implementation("com.squareup.okhttp3:logging-interceptor:4.2.1")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0")
|
||||
implementation("com.vdurmont:emoji-java:5.1.1")
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,96 @@
|
||||
package org.kraftwerk28.spigot_tg_bridge
|
||||
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import com.google.gson.annotations.SerializedName as Name
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import retrofit2.http.*
|
||||
import java.time.Duration
|
||||
import retrofit2.Retrofit
|
||||
|
||||
interface TgApiService {
|
||||
data class TgResponse<T>(val ok: Boolean, val result: T?, val description: String?)
|
||||
data class WebhookOptions(val drop_pending_updates: Boolean)
|
||||
|
||||
data class User(
|
||||
@Name("id") val id: Long,
|
||||
@Name("is_bot") val isBot: Boolean,
|
||||
@Name("first_name") val firstName: String,
|
||||
@Name("last_name") val lastName: String? = null,
|
||||
@Name("username") val username: String? = null,
|
||||
@Name("language_code") val languageCode: String? = null,
|
||||
)
|
||||
|
||||
data class Chat(
|
||||
val id: Long,
|
||||
val type: String,
|
||||
val title: String? = null,
|
||||
val username: String? = null,
|
||||
@Name("first_name") val firstName: String? = null,
|
||||
@Name("last_name") val lastName: String? = null,
|
||||
)
|
||||
|
||||
data class Message(
|
||||
@Name("message_id") val messageId: Long,
|
||||
val from: User? = null,
|
||||
@Name("sender_chat") val senderChat: Chat? = null,
|
||||
val date: Long,
|
||||
val chat: Chat,
|
||||
@Name("reply_to_message") val replyToMessage: Message? = null,
|
||||
val text: String? = null,
|
||||
)
|
||||
|
||||
data class Update(
|
||||
@Name("update_id") val updateId: Long,
|
||||
val message: Message? = null,
|
||||
)
|
||||
|
||||
data class BotCommand(val command: String, val description: String)
|
||||
data class SetMyCommands(val commands: List<BotCommand>)
|
||||
|
||||
@GET("deleteWebhook")
|
||||
suspend fun deleteWebhook(
|
||||
@Query("drop_pending_updates") dropPendingUpdates: Boolean
|
||||
): TgResponse<Boolean>
|
||||
|
||||
@GET("sendMessage?parse_mode=HTML")
|
||||
suspend fun sendMessage(
|
||||
@Query("chat_id") chatId: Long,
|
||||
@Query("text") text: String,
|
||||
@Query("reply_to_message_id") replyToMessageId: Long? = null,
|
||||
): TgResponse<Message>
|
||||
|
||||
@GET("getUpdates")
|
||||
suspend fun getUpdates(
|
||||
@Query("offset") offset: Long,
|
||||
@Query("limit") limit: Int = 100,
|
||||
@Query("timeout") timeout: Int = 0,
|
||||
): TgResponse<List<Update>>
|
||||
|
||||
@GET("getMe")
|
||||
suspend fun getMe(): TgResponse<User>
|
||||
|
||||
@POST("setMyCommands")
|
||||
suspend fun setMyCommands(
|
||||
@Body commands: SetMyCommands,
|
||||
): TgResponse<Boolean>
|
||||
|
||||
companion object {
|
||||
fun create(token: String): TgApiService {
|
||||
val interceptor = HttpLoggingInterceptor().apply {
|
||||
level = HttpLoggingInterceptor.Level.NONE;
|
||||
}
|
||||
val client = OkHttpClient
|
||||
.Builder()
|
||||
.addInterceptor(interceptor)
|
||||
.readTimeout(Duration.ZERO)
|
||||
.build();
|
||||
val r = Retrofit.Builder()
|
||||
.baseUrl("https://api.telegram.org/bot$token/")
|
||||
.client(client)
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.build()
|
||||
return r.create(TgApiService::class.java)
|
||||
}
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ import org.bukkit.event.player.AsyncPlayerChatEvent
|
||||
import org.bukkit.event.player.PlayerBedEnterEvent
|
||||
import org.bukkit.event.player.PlayerJoinEvent
|
||||
import org.bukkit.event.player.PlayerQuitEvent
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
class EventHandler(
|
||||
private val tgBot: TgBot,
|
||||
@ -17,16 +18,21 @@ class EventHandler(
|
||||
fun onPlayerChat(event: AsyncPlayerChatEvent) {
|
||||
if (!config.logFromMCtoTG) return
|
||||
event.run {
|
||||
tgBot.sendMessageToTelegram(
|
||||
message, player.displayName
|
||||
)
|
||||
measureTimeMillis {
|
||||
tgBot.sendMessageToTelegram(
|
||||
message, player.displayName
|
||||
)
|
||||
}
|
||||
.also {
|
||||
println("Time: $it")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
fun onPlayerJoin(event: PlayerJoinEvent) {
|
||||
if (!config.logJoinLeave || config.joinString == null) return
|
||||
val username = fullEscape(event.player.displayName)
|
||||
if (!config.logJoinLeave || config.joinString == null) return
|
||||
val username = event.player.displayName.fullEscape()
|
||||
val text = config.joinString!!.replace("%username%", username)
|
||||
tgBot.sendMessageToTelegram(text)
|
||||
}
|
||||
@ -34,7 +40,7 @@ class EventHandler(
|
||||
@EventHandler
|
||||
fun onPlayerLeave(event: PlayerQuitEvent) {
|
||||
if (!config.logJoinLeave || config.leaveString == null) return
|
||||
val username = fullEscape(event.player.displayName)
|
||||
val username = event.player.displayName.fullEscape()
|
||||
val text = config.leaveString!!.replace("%username%", username)
|
||||
tgBot.sendMessageToTelegram(text)
|
||||
}
|
||||
@ -43,7 +49,7 @@ class EventHandler(
|
||||
fun onPlayerDied(event: PlayerDeathEvent) {
|
||||
if (!config.logDeath) return
|
||||
event.deathMessage?.let {
|
||||
val username = fullEscape(event.entity.displayName)
|
||||
val username = event.entity.displayName.fullEscape()
|
||||
val text = it.replace(username, "<i>$username</i>")
|
||||
tgBot.sendMessageToTelegram(text)
|
||||
}
|
||||
|
@ -36,27 +36,27 @@ class Plugin : JavaPlugin() {
|
||||
|
||||
override fun onDisable() {
|
||||
if (!config.isEnabled) return
|
||||
config.serverStopMessage?.let { message ->
|
||||
tgBot?.sendMessageToTelegram(message)
|
||||
config.serverStopMessage?.let {
|
||||
tgBot?.sendMessageToTelegram(it)
|
||||
}
|
||||
logger.info("Plugin stopped.")
|
||||
}
|
||||
|
||||
fun sendMessageToMinecraft(text: String, username: String? = null) {
|
||||
var prepared = config.telegramMessageFormat
|
||||
.replace(C.MESSAGE_TEXT_PLACEHOLDER, escapeEmoji(text))
|
||||
username?.let {
|
||||
prepared = prepared
|
||||
.replace(C.USERNAME_PLACEHOLDER, escapeEmoji(it))
|
||||
}
|
||||
server.broadcastMessage(prepared)
|
||||
}
|
||||
fun sendMessageToMinecraft(text: String, username: String? = null) =
|
||||
config.telegramMessageFormat
|
||||
.replace(C.MESSAGE_TEXT_PLACEHOLDER, text.escapeEmoji())
|
||||
.run {
|
||||
username?.let { username ->
|
||||
replace(C.USERNAME_PLACEHOLDER, username.escapeEmoji())
|
||||
} ?: this
|
||||
}
|
||||
.also { server.broadcastMessage(it) }
|
||||
|
||||
fun reload() {
|
||||
logger.info(C.INFO.reloading)
|
||||
config.reload(this)
|
||||
tgBot?.stop()
|
||||
tgBot?.start(this, config)
|
||||
tgBot = TgBot(this, config)
|
||||
logger.info(C.INFO.reloadComplete)
|
||||
}
|
||||
}
|
||||
|
@ -1,96 +1,127 @@
|
||||
package org.kraftwerk28.spigot_tg_bridge
|
||||
|
||||
import com.github.kotlintelegrambot.*
|
||||
import com.github.kotlintelegrambot.dispatcher.command
|
||||
import com.github.kotlintelegrambot.dispatcher.text
|
||||
import com.github.kotlintelegrambot.entities.BotCommand
|
||||
import com.github.kotlintelegrambot.entities.ParseMode
|
||||
import com.github.kotlintelegrambot.entities.Update
|
||||
import com.github.kotlintelegrambot.logging.LogLevel
|
||||
import com.github.kotlintelegrambot.entities.ChatId
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import org.kraftwerk28.spigot_tg_bridge.Constants as C
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.*
|
||||
|
||||
class TgBot(private val plugin: Plugin, private val config: Configuration) {
|
||||
|
||||
private lateinit var bot: Bot
|
||||
|
||||
init {
|
||||
start(plugin, config)
|
||||
class TgBot(
|
||||
private val plugin: Plugin,
|
||||
private val config: Configuration,
|
||||
private val pollTimeout: Int = 30,
|
||||
) {
|
||||
private val api: TgApiService
|
||||
val updateChan = Channel<TgApiService.Update>()
|
||||
val scope = CoroutineScope(Dispatchers.Default)
|
||||
val pollJob: Job
|
||||
val handlerJob: Job
|
||||
var currentOffset: Long = -1
|
||||
var me: TgApiService.User
|
||||
var commandRegex: Regex
|
||||
val commandMap = config.commands.run {
|
||||
mapOf(
|
||||
online to ::onlineHandler,
|
||||
time to ::timeHandler,
|
||||
chatID to ::chatIdHandler,
|
||||
)
|
||||
}
|
||||
|
||||
fun start(plugin: Plugin, config: Configuration) {
|
||||
val slashRegex = "^/+".toRegex()
|
||||
val commands = config.commands
|
||||
init {
|
||||
api = TgApiService.create(config.botToken)
|
||||
runBlocking {
|
||||
me = api.getMe().result!!
|
||||
// I don't put optional @username in regex since bot is
|
||||
// only used in group chats
|
||||
commandRegex = """^\/(\w+)(?:@${me.username})$""".toRegex()
|
||||
|
||||
skipUpdates()
|
||||
bot = bot {
|
||||
token = config.botToken
|
||||
logLevel = LogLevel.None
|
||||
|
||||
val commandBindings = commands.let {
|
||||
mapOf(
|
||||
it.time to ::time,
|
||||
it.online to ::online,
|
||||
it.chatID to ::chatID
|
||||
)
|
||||
}.filterKeys { it != null }
|
||||
val commands = config.commands.run { listOf(time, online, chatID) }
|
||||
.zip(C.COMMAND_DESC.run {
|
||||
listOf(timeDesc, onlineDesc, chatIDDesc)
|
||||
})
|
||||
.map { TgApiService.BotCommand(it.first!!, it.second) }
|
||||
.let { TgApiService.SetMyCommands(it) }
|
||||
|
||||
dispatch {
|
||||
commandBindings.forEach { (text, handler) ->
|
||||
command(text!!.replace(slashRegex, "")) {
|
||||
handler(update)
|
||||
api.setMyCommands(commands)
|
||||
}
|
||||
pollJob = scope.launch {
|
||||
try {
|
||||
while (true) {
|
||||
try {
|
||||
pollUpdates()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
text { onText(update) }
|
||||
} catch (e: CancellationException) {}
|
||||
}
|
||||
handlerJob = scope.launch {
|
||||
try {
|
||||
while (true) {
|
||||
handleUpdate()
|
||||
}
|
||||
} catch (e: CancellationException) {}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun pollUpdates() {
|
||||
val updatesResponse = api
|
||||
.getUpdates(offset = currentOffset, timeout = pollTimeout)
|
||||
updatesResponse.result?.let { updates ->
|
||||
if (!updates.isEmpty()) {
|
||||
updates.forEach { updateChan.send(it) }
|
||||
currentOffset = updates.last().updateId + 1
|
||||
}
|
||||
}
|
||||
bot.setMyCommands(getBotCommands())
|
||||
}
|
||||
|
||||
config.webhookConfig?.let { _ ->
|
||||
plugin.logger.info("Running in webhook mode.")
|
||||
} ?: run {
|
||||
bot.startPolling()
|
||||
suspend fun handleUpdate() {
|
||||
val update = updateChan.receive()
|
||||
update.message?.text?.let {
|
||||
println("Text: $it")
|
||||
commandRegex.matchEntire(it)?.groupValues?.let {
|
||||
commandMap[it[1]]?.let { it(update) } ?: onTextHandler(update)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
bot.stopPolling()
|
||||
runBlocking {
|
||||
pollJob.cancelAndJoin()
|
||||
handlerJob.cancelAndJoin()
|
||||
}
|
||||
}
|
||||
|
||||
private fun time(update: Update) {
|
||||
private suspend fun timeHandler(update: TgApiService.Update) {
|
||||
val msg = update.message!!
|
||||
if (!config.allowedChats.contains(msg.chat.id)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (plugin.server.worlds.isEmpty()) {
|
||||
bot.sendMessage(
|
||||
ChatId.fromId(msg.chat.id),
|
||||
api.sendMessage(
|
||||
msg.chat.id,
|
||||
"No worlds available",
|
||||
replyToMessageId = msg.messageId
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
val t = plugin.server.worlds.first().time
|
||||
val text = when {
|
||||
t <= 12000 -> C.TIMES_OF_DAY.day
|
||||
t <= 13800 -> C.TIMES_OF_DAY.sunset
|
||||
t <= 22200 -> C.TIMES_OF_DAY.night
|
||||
t <= 24000 -> C.TIMES_OF_DAY.sunrise
|
||||
else -> ""
|
||||
} + " ($t)"
|
||||
// TODO: handle multiple worlds
|
||||
val time = plugin.server.worlds.first().time
|
||||
val text = C.TIMES_OF_DAY.run {
|
||||
when {
|
||||
time <= 12000 -> day
|
||||
time <= 13800 -> sunset
|
||||
time <= 22200 -> night
|
||||
time <= 24000 -> sunrise
|
||||
else -> ""
|
||||
}
|
||||
} + " ($time)"
|
||||
|
||||
bot.sendMessage(
|
||||
ChatId.fromId(msg.chat.id),
|
||||
text,
|
||||
replyToMessageId = msg.messageId,
|
||||
parseMode = ParseMode.HTML
|
||||
)
|
||||
api.sendMessage(msg.chat.id, text, replyToMessageId = msg.messageId)
|
||||
}
|
||||
|
||||
private fun online(update: Update) {
|
||||
private suspend fun onlineHandler(update: TgApiService.Update) {
|
||||
val msg = update.message!!
|
||||
if (!config.allowedChats.contains(msg.chat.id)) {
|
||||
return
|
||||
@ -99,62 +130,40 @@ class TgBot(private val plugin: Plugin, private val config: Configuration) {
|
||||
val playerList = plugin.server.onlinePlayers
|
||||
val playerStr = plugin.server
|
||||
.onlinePlayers
|
||||
.mapIndexed { i, s -> "${i + 1}. ${fullEscape(s.displayName)}" }
|
||||
.mapIndexed { i, s -> "${i + 1}. ${s.displayName.fullEscape()}" }
|
||||
.joinToString("\n")
|
||||
val text =
|
||||
if (playerList.isNotEmpty()) "${config.onlineString}:\n$playerStr"
|
||||
else config.nobodyOnlineString
|
||||
bot.sendMessage(
|
||||
ChatId.fromId(msg.chat.id),
|
||||
text,
|
||||
replyToMessageId = msg.messageId,
|
||||
parseMode = ParseMode.HTML
|
||||
)
|
||||
api.sendMessage(msg.chat.id, text, replyToMessageId = msg.messageId)
|
||||
}
|
||||
|
||||
private fun chatID(update: Update) {
|
||||
private suspend fun chatIdHandler(update: TgApiService.Update) {
|
||||
val msg = update.message!!
|
||||
val chatID = msg.chat.id
|
||||
val chatId = msg.chat.id
|
||||
val text = """
|
||||
Chat ID:
|
||||
<code>$chatID</code>
|
||||
<code>${chatId}</code>
|
||||
paste this id to <code>chats:</code> section in you config.yml file so it will look like this:
|
||||
""".trimIndent() +
|
||||
"\n\n<code>chats:\n # other ids...\n - ${chatID}</code>"
|
||||
bot.sendMessage(
|
||||
ChatId.fromId(chatID),
|
||||
text,
|
||||
parseMode = ParseMode.HTML,
|
||||
replyToMessageId = msg.messageId
|
||||
)
|
||||
"\n\n<code>chats:\n # other ids...\n - ${chatId}</code>"
|
||||
api.sendMessage(chatId, text, replyToMessageId = msg.messageId)
|
||||
}
|
||||
|
||||
fun sendMessageToTelegram(text: String, username: String? = null) {
|
||||
config.allowedChats.forEach { chatID ->
|
||||
username?.let {
|
||||
bot.sendMessage(
|
||||
ChatId.fromId(chatID),
|
||||
formatMsgFromMinecraft(username, text),
|
||||
parseMode = ParseMode.HTML,
|
||||
)
|
||||
} ?: run {
|
||||
bot.sendMessage(
|
||||
ChatId.fromId(chatID),
|
||||
text,
|
||||
parseMode = ParseMode.HTML,
|
||||
)
|
||||
val messageText = username?.let { formatMsgFromMinecraft(it, text) } ?: text
|
||||
config.allowedChats.forEach { chatId ->
|
||||
scope.launch {
|
||||
delay(1000)
|
||||
api.sendMessage(chatId, messageText)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onText(update: Update) {
|
||||
private suspend fun onTextHandler(update: TgApiService.Update) {
|
||||
if (!config.logFromTGtoMC) return
|
||||
val msg = update.message!!
|
||||
|
||||
// Suppress commands to be sent to Minecraft
|
||||
if (msg.text!!.startsWith("/")) return
|
||||
|
||||
plugin.sendMessageToMinecraft(msg.text!!, rawUserMention(msg.from!!))
|
||||
plugin.sendMessageToMinecraft(msg.text!!, msg.from!!.rawUserMention())
|
||||
}
|
||||
|
||||
private fun formatMsgFromMinecraft(
|
||||
@ -162,21 +171,6 @@ class TgBot(private val plugin: Plugin, private val config: Configuration) {
|
||||
text: String
|
||||
): String =
|
||||
config.minecraftMessageFormat
|
||||
.replace("%username%", fullEscape(username))
|
||||
.replace("%message%", escapeHTML(text))
|
||||
|
||||
private fun getBotCommands(): List<BotCommand> {
|
||||
val cmdList = config.commands.run { listOfNotNull(time, online, chatID) }
|
||||
val descList = C.COMMAND_DESC.run { listOf(timeDesc, onlineDesc, chatIDDesc) }
|
||||
return cmdList.zip(descList).map { BotCommand(it.first, it.second) }
|
||||
}
|
||||
|
||||
private fun skipUpdates() {
|
||||
// Creates a temporary bot w/ 0 timeout to skip updates
|
||||
bot {
|
||||
token = config.botToken
|
||||
timeout = 0
|
||||
logLevel = LogLevel.None
|
||||
}.skipUpdates()
|
||||
}
|
||||
.replace("%username%", username.fullEscape())
|
||||
.replace("%message%", text.escapeHtml())
|
||||
}
|
||||
|
@ -1,35 +1,34 @@
|
||||
package org.kraftwerk28.spigot_tg_bridge
|
||||
|
||||
import com.github.kotlintelegrambot.Bot
|
||||
import com.github.kotlintelegrambot.entities.Update
|
||||
import com.github.kotlintelegrambot.entities.User
|
||||
import com.vdurmont.emoji.EmojiParser
|
||||
|
||||
fun Bot.skipUpdates(lastUpdateID: Long = 0) {
|
||||
val newUpdates = getUpdates(lastUpdateID)
|
||||
// fun Bot.skipUpdates(lastUpdateID: Long = 0) {
|
||||
// val newUpdates = getUpdates(lastUpdateID)
|
||||
|
||||
if (newUpdates.isNotEmpty()) {
|
||||
val lastUpd = newUpdates.last()
|
||||
if (lastUpd !is Update) return
|
||||
return skipUpdates(lastUpd.updateId + 1)
|
||||
}
|
||||
}
|
||||
// if (newUpdates.isNotEmpty()) {
|
||||
// val lastUpd = newUpdates.last()
|
||||
// if (lastUpd !is Update) return
|
||||
// return skipUpdates(lastUpd.updateId + 1)
|
||||
// }
|
||||
// }
|
||||
|
||||
fun String.escapeHtml() =
|
||||
this.replace("&", "&").replace(">", ">").replace("<", "<")
|
||||
|
||||
fun escapeHTML(s: String) = s
|
||||
fun String.escapeHtml() = this
|
||||
.replace("&", "&")
|
||||
.replace(">", ">")
|
||||
.replace("<", "<")
|
||||
|
||||
fun escapeColorCodes(s: String) = s.replace("\u00A7.".toRegex(), "")
|
||||
fun String.escapeHTML() = this
|
||||
.replace("&", "&")
|
||||
.replace(">", ">")
|
||||
.replace("<", "<")
|
||||
|
||||
fun fullEscape(s: String) = escapeColorCodes(escapeHTML(s))
|
||||
fun String.escapeColorCodes() = replace("\u00A7.".toRegex(), "")
|
||||
|
||||
fun escapeEmoji(text: String) = EmojiParser.parseToAliases(text)
|
||||
fun String.fullEscape() = escapeHTML().escapeColorCodes()
|
||||
|
||||
fun rawUserMention(user: User): String =
|
||||
(if (user.firstName.length < 2) null else user.firstName)
|
||||
?: user.username
|
||||
?: user.lastName!!
|
||||
fun String.escapeEmoji() = EmojiParser.parseToAliases(this)
|
||||
|
||||
fun TgApiService.User.rawUserMention(): String =
|
||||
(if (firstName.length < 2) null else firstName)
|
||||
?: username
|
||||
?: lastName!!
|
||||
|
Loading…
Reference in New Issue
Block a user