Merge pull request #311 from InsanusMokrassar/22.0.0

22.0.0
This commit is contained in:
InsanusMokrassar 2024-12-09 08:54:48 +06:00 committed by GitHub
commit 6a61da2eb7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 602 additions and 267 deletions

View File

@ -11,6 +11,9 @@ buildscript {
plugins { plugins {
id "org.jetbrains.kotlin.multiplatform" id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization" id "org.jetbrains.kotlin.plugin.serialization"
id "org.jetbrains.kotlin.plugin.compose" version "$kotlin_version"
id "org.jetbrains.compose" version "$compose_version"
} }
apply plugin: 'application' apply plugin: 'application'
@ -27,12 +30,15 @@ kotlin {
dependencies { dependencies {
implementation kotlin('stdlib') implementation kotlin('stdlib')
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$serialization_version" implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$serialization_version"
implementation "dev.inmo:tgbotapi.core:$telegram_bot_api_version"
implementation compose.runtime
} }
} }
jsMain { jsMain {
dependencies { dependencies {
implementation "dev.inmo:tgbotapi.webapps:$telegram_bot_api_version" implementation "dev.inmo:tgbotapi.webapps:$telegram_bot_api_version"
implementation compose.web.core
} }
} }
@ -41,6 +47,7 @@ kotlin {
implementation "dev.inmo:tgbotapi:$telegram_bot_api_version" implementation "dev.inmo:tgbotapi:$telegram_bot_api_version"
implementation "dev.inmo:micro_utils.ktor.server:$micro_utils_version" implementation "dev.inmo:micro_utils.ktor.server:$micro_utils_version"
implementation "io.ktor:ktor-server-cio:$ktor_version" implementation "io.ktor:ktor-server-cio:$ktor_version"
implementation compose.desktop.currentOs
} }
} }
} }

View File

@ -0,0 +1,3 @@
import dev.inmo.tgbotapi.types.CustomEmojiId
val CustomEmojiIdToSet = CustomEmojiId("5424939566278649034")

View File

@ -1,22 +1,35 @@
import androidx.compose.runtime.*
import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions
import dev.inmo.tgbotapi.types.CustomEmojiId
import dev.inmo.tgbotapi.types.userIdField
import dev.inmo.tgbotapi.types.webAppQueryIdField import dev.inmo.tgbotapi.types.webAppQueryIdField
import dev.inmo.tgbotapi.webapps.* import dev.inmo.tgbotapi.webapps.*
import dev.inmo.tgbotapi.webapps.accelerometer.AccelerometerStartParams
import dev.inmo.tgbotapi.webapps.cloud.* import dev.inmo.tgbotapi.webapps.cloud.*
import dev.inmo.tgbotapi.webapps.events.*
import dev.inmo.tgbotapi.webapps.gyroscope.GyroscopeStartParams
import dev.inmo.tgbotapi.webapps.haptic.HapticFeedbackStyle import dev.inmo.tgbotapi.webapps.haptic.HapticFeedbackStyle
import dev.inmo.tgbotapi.webapps.haptic.HapticFeedbackType import dev.inmo.tgbotapi.webapps.haptic.HapticFeedbackType
import dev.inmo.tgbotapi.webapps.orientation.DeviceOrientationStartParams
import dev.inmo.tgbotapi.webapps.popup.* import dev.inmo.tgbotapi.webapps.popup.*
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import io.ktor.client.request.* import io.ktor.client.request.*
import io.ktor.client.statement.bodyAsText import io.ktor.client.statement.bodyAsText
import io.ktor.http.* import io.ktor.http.*
import io.ktor.http.content.TextContent import io.ktor.http.content.TextContent
import kotlinx.browser.document
import kotlinx.browser.window import kotlinx.browser.window
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.dom.appendElement import kotlinx.dom.appendElement
import kotlinx.dom.appendText import kotlinx.dom.appendText
import kotlinx.dom.clear
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import org.jetbrains.compose.web.attributes.InputType
import org.jetbrains.compose.web.css.DisplayStyle
import org.jetbrains.compose.web.css.Color as ComposeColor
import org.jetbrains.compose.web.css.backgroundColor
import org.jetbrains.compose.web.css.display
import org.jetbrains.compose.web.dom.*
import org.jetbrains.compose.web.dom.Text
import org.jetbrains.compose.web.renderComposable
import org.w3c.dom.* import org.w3c.dom.*
import kotlin.random.Random import kotlin.random.Random
import kotlin.random.nextUBytes import kotlin.random.nextUBytes
@ -32,277 +45,356 @@ fun main() {
val client = HttpClient() val client = HttpClient()
val baseUrl = window.location.origin.removeSuffix("/") val baseUrl = window.location.origin.removeSuffix("/")
window.onload = { renderComposable("root") {
val scope = CoroutineScope(Dispatchers.Default) val scope = rememberCoroutineScope()
runCatching { val isSafeState = remember { mutableStateOf<Boolean?>(null) }
val logsState = remember { mutableStateListOf<Any?>() }
scope.launchSafelyWithoutExceptions { // Text(window.location.href)
val response = client.post("$baseUrl/check") { // P()
setBody(
Json.encodeToString( LaunchedEffect(baseUrl) {
WebAppDataWrapper.serializer(), val response = client.post("$baseUrl/check") {
WebAppDataWrapper(webApp.initData, webApp.initDataUnsafe.hash) setBody(
) Json.encodeToString(
WebAppDataWrapper.serializer(),
WebAppDataWrapper(webApp.initData, webApp.initDataUnsafe.hash)
)
)
}
val dataIsSafe = response.bodyAsText().toBoolean()
if (dataIsSafe) {
isSafeState.value = true
logsState.add("Data is safe")
} else {
isSafeState.value = false
logsState.add("Data is unsafe")
}
logsState.add(
webApp.initDataUnsafe.chat.toString()
)
}
Text(
when (isSafeState.value) {
null -> "Checking safe state..."
true -> "Data is safe"
false -> "Data is unsafe"
}
)
P()
Text("Chat from WebAppInitData: ${webApp.initDataUnsafe.chat}")
val emojiStatusAccessState = remember { mutableStateOf(false) }
webApp.onEmojiStatusAccessRequested {
emojiStatusAccessState.value = it.isAllowed
}
Button({
onClick {
webApp.requestEmojiStatusAccess()
}
}) {
Text("Request custom emoji status access")
}
if (emojiStatusAccessState.value) {
Button({
onClick {
webApp.setEmojiStatus(CustomEmojiIdToSet/* android custom emoji id */)
}
}) {
Text("Set custom emoji status")
}
val userId = webApp.initDataUnsafe.user ?.id
userId ?.let { userId ->
Button({
onClick {
scope.launchSafelyWithoutExceptions {
client.post("$baseUrl/setCustomEmoji") {
parameter(userIdField, userId.long)
setBody(
Json.encodeToString(
WebAppDataWrapper.serializer(),
WebAppDataWrapper(webApp.initData, webApp.initDataUnsafe.hash)
)
)
}
}
}
}) {
Text("Set custom emoji status via bot")
}
}
}
Button({
onClick {
scope.launchSafelyWithoutExceptions {
handleResult({ "Clicked" }) {
client.post("${window.location.origin.removeSuffix("/")}/inline") {
parameter(webAppQueryIdField, it)
setBody(TextContent("Clicked", ContentType.Text.Plain))
logsState.add(url.build().toString())
}.coroutineContext.job.join()
}
}
}
}) {
Text("Answer in chat button")
}
P()
Text("Allow to write in private messages: ${webApp.initDataUnsafe.user ?.allowsWriteToPM ?: "User unavailable"}")
P()
Text("Alerts:")
Button({
onClick {
webApp.showPopup(
PopupParams(
"It is sample title of default button",
"It is sample message of default button",
DefaultPopupButton("default", "Default button"),
OkPopupButton("ok"),
DestructivePopupButton("destructive", "Destructive button")
)
) {
logsState.add(
when (it) {
"default" -> "You have clicked default button in popup"
"ok" -> "You have clicked ok button in popup"
"destructive" -> "You have clicked destructive button in popup"
else -> "I can't imagine where you take button with id $it"
}
) )
} }
val dataIsSafe = response.bodyAsText().toBoolean()
document.body ?.log(
if (dataIsSafe) {
"Data is safe"
} else {
"Data is unsafe"
}
)
document.body ?.log(
webApp.initDataUnsafe.chat.toString()
)
} }
}) {
document.body ?.appendElement("button") { Text("Popup")
addEventListener("click", { }
scope.launchSafelyWithoutExceptions { Button({
handleResult({ "Clicked" }) { onClick {
client.post("${window.location.origin.removeSuffix("/")}/inline") { webApp.showAlert(
parameter(webAppQueryIdField, it) "This is alert message"
setBody(TextContent("Clicked", ContentType.Text.Plain)) ) {
document.body ?.log(url.build().toString()) logsState.add(
}.coroutineContext.job.join() "You have closed alert"
} )
}
})
appendText("Answer in chat button")
} ?: window.alert("Unable to load body")
document.body ?.appendElement("p", {})
document.body ?.appendText("Allow to write in private messages: ${webApp.initDataUnsafe.user ?.allowsWriteToPM ?: "User unavailable"}")
document.body ?.appendElement("p", {})
document.body ?.appendText("Alerts:")
document.body ?.appendElement("button") {
addEventListener("click", {
webApp.showPopup(
PopupParams(
"It is sample title of default button",
"It is sample message of default button",
DefaultPopupButton("default", "Default button"),
OkPopupButton("ok"),
DestructivePopupButton("destructive", "Destructive button")
)
) {
document.body ?.log(
when (it) {
"default" -> "You have clicked default button in popup"
"ok" -> "You have clicked ok button in popup"
"destructive" -> "You have clicked destructive button in popup"
else -> "I can't imagine where you take button with id $it"
}
)
}
})
appendText("Popup")
} ?: window.alert("Unable to load body")
document.body ?.appendElement("button") {
addEventListener("click", {
webApp.showAlert(
"This is alert message"
) {
document.body ?.log(
"You have closed alert"
)
}
})
appendText("Alert")
} ?: window.alert("Unable to load body")
document.body ?.appendElement("p", {})
document.body ?.appendElement("button") {
addEventListener("click", { webApp.requestWriteAccess() })
appendText("Request write access without callback")
} ?: window.alert("Unable to load body")
document.body ?.appendElement("button") {
addEventListener("click", { webApp.requestWriteAccess { document.body ?.log("Write access request result: $it") } })
appendText("Request write access with callback")
} ?: window.alert("Unable to load body")
document.body ?.appendElement("p", {})
document.body ?.appendElement("button") {
addEventListener("click", { webApp.requestContact() })
appendText("Request contact without callback")
} ?: window.alert("Unable to load body")
document.body ?.appendElement("button") {
addEventListener("click", { webApp.requestContact { document.body ?.log("Contact request result: $it") } })
appendText("Request contact with callback")
} ?: window.alert("Unable to load body")
document.body ?.appendElement("p", {})
document.body ?.appendElement("button") {
addEventListener("click", {
webApp.showConfirm(
"This is confirm message"
) {
document.body ?.log(
"You have pressed \"${if (it) "Ok" else "Cancel"}\" in confirm"
)
}
})
appendText("Confirm")
} ?: window.alert("Unable to load body")
document.body ?.appendElement("p", {})
document.body ?.appendElement("button") {
fun updateText() {
textContent = if (webApp.isClosingConfirmationEnabled) {
"Disable closing confirmation"
} else {
"Enable closing confirmation"
}
} }
addEventListener("click", { }
webApp.toggleClosingConfirmation() }) {
updateText() Text("Alert")
}) }
updateText()
} ?: window.alert("Unable to load body")
document.body ?.appendElement("p", {}) P()
Button({
document.body ?.appendElement("button") { onClick {
fun updateHeaderColor() { webApp.requestWriteAccess()
val (r, g, b) = Random.nextUBytes(3) }
val hex = Color.Hex(r, g, b) }) {
webApp.setHeaderColor(hex) Text("Request write access without callback")
(this as? HTMLButtonElement) ?.style ?.backgroundColor = hex.value }
textContent = "Header color: ${webApp.headerColor ?.uppercase()} (click to change)" Button({
onClick {
webApp.requestWriteAccess {
logsState.add("Write access request result: $it")
} }
addEventListener("click", { }
updateHeaderColor() }) {
}) Text("Request write access with callback")
}
P()
Button({
onClick {
webApp.requestContact()
}
}) {
Text("Request contact without callback")
}
Button({
onClick {
webApp.requestContact { logsState.add("Contact request result: $it") }
}
}) {
Text("Request contact with callback")
}
P()
Button({
onClick {
webApp.showConfirm(
"This is confirm message"
) {
logsState.add(
"You have pressed \"${if (it) "Ok" else "Cancel"}\" in confirm"
)
}
}
}) {
Text("Confirm")
}
P()
val isClosingConfirmationEnabledState = remember { mutableStateOf(webApp.isClosingConfirmationEnabled) }
Button({
onClick {
webApp.toggleClosingConfirmation()
isClosingConfirmationEnabledState.value = webApp.isClosingConfirmationEnabled
}
}) {
Text(
if (isClosingConfirmationEnabledState.value) {
"Disable closing confirmation"
} else {
"Enable closing confirmation"
}
)
}
P()
val headerColor = remember { mutableStateOf<Color.Hex>(Color.Hex("#000000")) }
fun updateHeaderColor() {
val (r, g, b) = Random.nextUBytes(3)
headerColor.value = Color.Hex(r, g, b)
webApp.setHeaderColor(headerColor.value)
}
DisposableEffect(0) {
updateHeaderColor()
onDispose { }
}
Button({
style {
backgroundColor(ComposeColor(headerColor.value.value))
}
onClick {
updateHeaderColor() updateHeaderColor()
} ?: window.alert("Unable to load body") }
}) {
key(headerColor.value) {
Text("Header color: ${webApp.headerColor ?.uppercase()} (click to change)")
}
}
document.body ?.appendElement("p", {}) P()
document.body ?.appendElement("button") { val backgroundColor = remember { mutableStateOf<Color.Hex>(Color.Hex("#000000")) }
fun updateBackgroundColor() { fun updateBackgroundColor() {
val (r, g, b) = Random.nextUBytes(3) val (r, g, b) = Random.nextUBytes(3)
val hex = Color.Hex(r, g, b) backgroundColor.value = Color.Hex(r, g, b)
webApp.setBackgroundColor(hex) webApp.setBackgroundColor(backgroundColor.value)
(this as? HTMLButtonElement) ?.style ?.backgroundColor = hex.value }
textContent = "Background color: ${webApp.backgroundColor ?.uppercase()} (click to change)" DisposableEffect(0) {
} updateBackgroundColor()
addEventListener("click", { onDispose { }
updateBackgroundColor() }
}) Button({
style {
backgroundColor(ComposeColor(backgroundColor.value.value))
}
onClick {
updateBackgroundColor() updateBackgroundColor()
} ?: window.alert("Unable to load body") }
}) {
key(backgroundColor.value) {
Text("Background color: ${webApp.backgroundColor ?.uppercase()} (click to change)")
}
}
document.body ?.appendElement("p", {}) P()
document.body ?.appendElement("button") { val bottomBarColor = remember { mutableStateOf<Color.Hex>(Color.Hex("#000000")) }
fun updateBottomBarColor() { fun updateBottomBarColor() {
val (r, g, b) = Random.nextUBytes(3) val (r, g, b) = Random.nextUBytes(3)
val hex = Color.Hex(r, g, b) bottomBarColor.value = Color.Hex(r, g, b)
webApp.setBottomBarColor(hex) webApp.setBottomBarColor(bottomBarColor.value)
(this as? HTMLButtonElement) ?.style ?.backgroundColor = hex.value }
textContent = "Bottom bar color: ${webApp.bottomBarColor ?.uppercase()} (click to change)" DisposableEffect(0) {
} updateBottomBarColor()
addEventListener("click", { onDispose { }
updateBottomBarColor() }
}) Button({
style {
backgroundColor(ComposeColor(bottomBarColor.value.value))
}
onClick {
updateBottomBarColor() updateBottomBarColor()
} ?: window.alert("Unable to load body") }
}) {
key(bottomBarColor.value) {
Text("Bottom bar color: ${webApp.bottomBarColor ?.uppercase()} (click to change)")
}
}
document.body ?.appendElement("p", {}) P()
fun Element.updateCloudStorageContent() { val storageTrigger = remember { mutableStateOf<List<Pair<CloudStorageKey, CloudStorageValue>>>(emptyList()) }
clear() fun updateCloudStorage() {
webApp.cloudStorage.getAll { webApp.cloudStorage.getAll {
it.onSuccess { it.onSuccess {
document.body ?.log(it.toString()) storageTrigger.value = it.toList().sortedBy { it.first.key }
appendElement("label") { textContent = "Cloud storage" }
appendElement("p", {})
it.forEach { (k, v) ->
appendElement("div") {
val kInput = appendElement("input", {}) as HTMLInputElement
val vInput = appendElement("input", {}) as HTMLInputElement
kInput.value = k.key
vInput.value = v.value
appendElement("button") {
addEventListener("click", {
if (k.key == kInput.value) {
webApp.cloudStorage.set(k.key, vInput.value) {
document.body ?.log(it.toString())
this@updateCloudStorageContent.updateCloudStorageContent()
}
} else {
webApp.cloudStorage.remove(k.key) {
it.onSuccess {
webApp.cloudStorage.set(kInput.value, vInput.value) {
document.body ?.log(it.toString())
this@updateCloudStorageContent.updateCloudStorageContent()
}
}
}
}
})
this.textContent = "Save"
}
}
appendElement("p", {})
}
appendElement("label") { textContent = "Cloud storage: add new" }
appendElement("p", {})
appendElement("div") {
val kInput = appendElement("input", {}) as HTMLInputElement
appendElement("button") {
textContent = "Add key"
addEventListener("click", {
webApp.cloudStorage.set(kInput.value, kInput.value) {
document.body ?.log(it.toString())
this@updateCloudStorageContent.updateCloudStorageContent()
}
})
}
}
appendElement("p", {})
}.onFailure {
document.body ?.log(it.stackTraceToString())
}
} }
} }
val cloudStorageContentDiv = document.body ?.appendElement("div") {} as HTMLDivElement }
key(storageTrigger.value) {
document.body ?.appendElement("p", {}) storageTrigger.value.forEach { (key, value) ->
val keyState = remember { mutableStateOf(key.key) }
val valueState = remember { mutableStateOf(value.value) }
Input(InputType.Text) {
value(key.key)
onInput { keyState.value = it.value }
}
Input(InputType.Text) {
value(value.value)
onInput { valueState.value = it.value }
}
Button({
onClick {
if (key.key != keyState.value) {
webApp.cloudStorage.remove(key)
}
webApp.cloudStorage.set(keyState.value, valueState.value)
updateCloudStorage()
}
}) {
Text("Save")
}
}
let { // new element adding
val keyState = remember { mutableStateOf("") }
val valueState = remember { mutableStateOf("") }
Input(InputType.Text) {
onInput { keyState.value = it.value }
}
Input(InputType.Text) {
onInput { valueState.value = it.value }
}
Button({
onClick {
webApp.cloudStorage.set(keyState.value, valueState.value)
updateCloudStorage()
}
}) {
Text("Save")
}
}
}
remember {
webApp.apply { webApp.apply {
onThemeChanged { onThemeChanged {
document.body ?.log("Theme changed: ${webApp.themeParams}") logsState.add("Theme changed: ${webApp.themeParams}")
} }
onViewportChanged { onViewportChanged {
document.body ?.log("Viewport changed: ${it.isStateStable}") logsState.add("Viewport changed: ${it}")
} }
backButton.apply { backButton.apply {
onClick { onClick {
document.body ?.log("Back button clicked") logsState.add("Back button clicked")
hapticFeedback.impactOccurred( hapticFeedback.impactOccurred(
HapticFeedbackStyle.Heavy HapticFeedbackStyle.Heavy
) )
@ -312,7 +404,7 @@ fun main() {
mainButton.apply { mainButton.apply {
setText("Main button") setText("Main button")
onClick { onClick {
document.body ?.log("Main button clicked") logsState.add("Main button clicked")
hapticFeedback.notificationOccurred( hapticFeedback.notificationOccurred(
HapticFeedbackType.Success HapticFeedbackType.Success
) )
@ -322,7 +414,7 @@ fun main() {
secondaryButton.apply { secondaryButton.apply {
setText("Secondary button") setText("Secondary button")
onClick { onClick {
document.body ?.log("Secondary button clicked") logsState.add("Secondary button clicked")
hapticFeedback.notificationOccurred( hapticFeedback.notificationOccurred(
HapticFeedbackType.Warning HapticFeedbackType.Warning
) )
@ -330,22 +422,231 @@ fun main() {
show() show()
} }
onSettingsButtonClicked { onSettingsButtonClicked {
document.body ?.log("Settings button clicked") logsState.add("Settings button clicked")
} }
onWriteAccessRequested { onWriteAccessRequested {
document.body ?.log("Write access request result: $it") logsState.add("Write access request result: $it")
} }
onContactRequested { onContactRequested {
document.body ?.log("Contact request result: $it") logsState.add("Contact request result: $it")
} }
} }
webApp.ready() }
document.body ?.appendElement("input", { P()
(this as HTMLInputElement).value = window.location.href
}) let { // Accelerometer
cloudStorageContentDiv.updateCloudStorageContent() val enabledState = remember { mutableStateOf(webApp.accelerometer.isStarted) }
}.onFailure { webApp.onAccelerometerStarted { enabledState.value = true }
window.alert(it.stackTraceToString()) webApp.onAccelerometerStopped { enabledState.value = false }
Button({
onClick {
if (enabledState.value) {
webApp.accelerometer.stop { }
} else {
webApp.accelerometer.start(AccelerometerStartParams(200))
}
}
}) {
Text("${if (enabledState.value) "Stop" else "Start"} accelerometer")
}
val xState = remember { mutableStateOf(webApp.accelerometer.x) }
val yState = remember { mutableStateOf(webApp.accelerometer.y) }
val zState = remember { mutableStateOf(webApp.accelerometer.z) }
fun updateValues() {
xState.value = webApp.accelerometer.x
yState.value = webApp.accelerometer.y
zState.value = webApp.accelerometer.z
}
remember {
updateValues()
}
webApp.onAccelerometerChanged {
updateValues()
}
if (enabledState.value) {
P()
Text("x: ${xState.value}")
P()
Text("y: ${yState.value}")
P()
Text("z: ${zState.value}")
}
}
P()
let { // Gyroscope
val enabledState = remember { mutableStateOf(webApp.gyroscope.isStarted) }
webApp.onGyroscopeStarted { enabledState.value = true }
webApp.onGyroscopeStopped { enabledState.value = false }
Button({
onClick {
if (enabledState.value) {
webApp.gyroscope.stop { }
} else {
webApp.gyroscope.start(GyroscopeStartParams(200))
}
}
}) {
Text("${if (enabledState.value) "Stop" else "Start"} gyroscope")
}
val xState = remember { mutableStateOf(webApp.gyroscope.x) }
val yState = remember { mutableStateOf(webApp.gyroscope.y) }
val zState = remember { mutableStateOf(webApp.gyroscope.z) }
fun updateValues() {
xState.value = webApp.gyroscope.x
yState.value = webApp.gyroscope.y
zState.value = webApp.gyroscope.z
}
remember {
updateValues()
}
webApp.onGyroscopeChanged {
updateValues()
}
if (enabledState.value) {
P()
Text("x: ${xState.value}")
P()
Text("y: ${yState.value}")
P()
Text("z: ${zState.value}")
}
}
P()
let { // DeviceOrientation
val enabledState = remember { mutableStateOf(webApp.deviceOrientation.isStarted) }
webApp.onDeviceOrientationStarted { enabledState.value = true }
webApp.onDeviceOrientationStopped { enabledState.value = false }
Button({
onClick {
if (enabledState.value) {
webApp.deviceOrientation.stop { }
} else {
webApp.deviceOrientation.start(DeviceOrientationStartParams(200))
}
}
}) {
Text("${if (enabledState.value) "Stop" else "Start"} deviceOrientation")
}
val alphaState = remember { mutableStateOf(webApp.deviceOrientation.alpha) }
val betaState = remember { mutableStateOf(webApp.deviceOrientation.beta) }
val gammaState = remember { mutableStateOf(webApp.deviceOrientation.gamma) }
fun updateValues() {
alphaState.value = webApp.deviceOrientation.alpha
betaState.value = webApp.deviceOrientation.beta
gammaState.value = webApp.deviceOrientation.gamma
}
remember {
updateValues()
}
webApp.onDeviceOrientationChanged {
updateValues()
}
if (enabledState.value) {
P()
Text("alpha: ${alphaState.value}")
P()
Text("beta: ${betaState.value}")
P()
Text("gamma: ${gammaState.value}")
}
}
P()
EventType.values().forEach { eventType ->
when (eventType) {
EventType.AccelerometerChanged -> webApp.onAccelerometerChanged { /*logsState.add("AccelerometerChanged") /* see accelerometer block */ */ }
EventType.AccelerometerFailed -> webApp.onAccelerometerFailed {
logsState.add(it.error)
}
EventType.AccelerometerStarted -> webApp.onAccelerometerStarted { logsState.add("AccelerometerStarted") }
EventType.AccelerometerStopped -> webApp.onAccelerometerStopped { logsState.add("AccelerometerStopped") }
EventType.Activated -> webApp.onActivated { logsState.add("Activated") }
EventType.BackButtonClicked -> webApp.onBackButtonClicked { logsState.add("BackButtonClicked") }
EventType.BiometricAuthRequested -> webApp.onBiometricAuthRequested {
logsState.add(it.isAuthenticated)
}
EventType.BiometricManagerUpdated -> webApp.onBiometricManagerUpdated { logsState.add("BiometricManagerUpdated") }
EventType.BiometricTokenUpdated -> webApp.onBiometricTokenUpdated {
logsState.add(it.isUpdated)
}
EventType.ClipboardTextReceived -> webApp.onClipboardTextReceived {
logsState.add(it.data)
}
EventType.ContactRequested -> webApp.onContactRequested {
logsState.add(it.status)
}
EventType.ContentSafeAreaChanged -> webApp.onContentSafeAreaChanged { logsState.add("ContentSafeAreaChanged") }
EventType.Deactivated -> webApp.onDeactivated { logsState.add("Deactivated") }
EventType.DeviceOrientationChanged -> webApp.onDeviceOrientationChanged { /*logsState.add("DeviceOrientationChanged")*//* see accelerometer block */ }
EventType.DeviceOrientationFailed -> webApp.onDeviceOrientationFailed {
logsState.add(it.error)
}
EventType.DeviceOrientationStarted -> webApp.onDeviceOrientationStarted { logsState.add("DeviceOrientationStarted") }
EventType.DeviceOrientationStopped -> webApp.onDeviceOrientationStopped { logsState.add("DeviceOrientationStopped") }
EventType.EmojiStatusAccessRequested -> webApp.onEmojiStatusAccessRequested {
logsState.add(it.status)
}
EventType.EmojiStatusFailed -> webApp.onEmojiStatusFailed {
logsState.add(it.error)
}
EventType.EmojiStatusSet -> webApp.onEmojiStatusSet { logsState.add("EmojiStatusSet") }
EventType.FileDownloadRequested -> webApp.onFileDownloadRequested {
logsState.add(it.status)
}
EventType.FullscreenChanged -> webApp.onFullscreenChanged { logsState.add("FullscreenChanged") }
EventType.FullscreenFailed -> webApp.onFullscreenFailed {
logsState.add(it.error)
}
EventType.GyroscopeChanged -> webApp.onGyroscopeChanged { /*logsState.add("GyroscopeChanged")*//* see gyroscope block */ }
EventType.GyroscopeFailed -> webApp.onGyroscopeFailed {
logsState.add(it.error)
}
EventType.GyroscopeStarted -> webApp.onGyroscopeStarted { logsState.add("GyroscopeStarted")/* see accelerometer block */ }
EventType.GyroscopeStopped -> webApp.onGyroscopeStopped { logsState.add("GyroscopeStopped") }
EventType.HomeScreenAdded -> webApp.onHomeScreenAdded { logsState.add("HomeScreenAdded") }
EventType.HomeScreenChecked -> webApp.onHomeScreenChecked {
logsState.add(it.status)
}
EventType.InvoiceClosed -> webApp.onInvoiceClosed { url, status ->
logsState.add(url)
logsState.add(status)
}
EventType.LocationManagerUpdated -> webApp.onLocationManagerUpdated { logsState.add("LocationManagerUpdated") }
EventType.LocationRequested -> webApp.onLocationRequested {
logsState.add(it.locationData)
}
EventType.MainButtonClicked -> webApp.onMainButtonClicked { logsState.add("MainButtonClicked") }
EventType.PopupClosed -> webApp.onPopupClosed {
logsState.add(it.buttonId)
}
EventType.QrTextReceived -> webApp.onQrTextReceived {
logsState.add(it.data)
}
EventType.SafeAreaChanged -> webApp.onSafeAreaChanged { logsState.add("SafeAreaChanged") }
EventType.ScanQrPopupClosed -> webApp.onScanQrPopupClosed { logsState.add("ScanQrPopupClosed") }
EventType.SecondaryButtonClicked -> webApp.onSecondaryButtonClicked { logsState.add("SecondaryButtonClicked") }
EventType.SettingsButtonClicked -> webApp.onSettingsButtonClicked { logsState.add("SettingsButtonClicked") }
EventType.ShareMessageFailed -> webApp.onShareMessageFailed {
logsState.add(it.error)
}
EventType.ShareMessageSent -> webApp.onShareMessageSent { logsState.add("ShareMessageSent") }
EventType.ThemeChanged -> webApp.onThemeChanged { logsState.add("ThemeChanged") }
EventType.ViewportChanged -> webApp.onViewportChanged {
logsState.add(it)
}
EventType.WriteAccessRequested -> webApp.onWriteAccessRequested {
logsState.add(it.status)
}
}
}
logsState.forEach {
P { Text(it.toString()) }
} }
} }
} }

View File

@ -10,6 +10,7 @@
<title>Web App Example</title> <title>Web App Example</title>
</head> </head>
<body> <body>
<div id="root"></div>
<script type="application/javascript" src="https://telegram.org/js/telegram-web-app.js"></script> <script type="application/javascript" src="https://telegram.org/js/telegram-web-app.js"></script>
<script type="application/javascript" src="WebApp.js"></script> <script type="application/javascript" src="WebApp.js"></script>
</body> </body>

View File

@ -6,6 +6,7 @@ import dev.inmo.tgbotapi.extensions.api.bot.getMe
import dev.inmo.tgbotapi.extensions.api.bot.setMyCommands import dev.inmo.tgbotapi.extensions.api.bot.setMyCommands
import dev.inmo.tgbotapi.extensions.api.send.reply import dev.inmo.tgbotapi.extensions.api.send.reply
import dev.inmo.tgbotapi.extensions.api.send.send import dev.inmo.tgbotapi.extensions.api.send.send
import dev.inmo.tgbotapi.extensions.api.set.setUserEmojiStatus
import dev.inmo.tgbotapi.extensions.api.telegramBot import dev.inmo.tgbotapi.extensions.api.telegramBot
import dev.inmo.tgbotapi.extensions.behaviour_builder.buildBehaviourWithLongPolling import dev.inmo.tgbotapi.extensions.behaviour_builder.buildBehaviourWithLongPolling
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onBaseInlineQuery import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onBaseInlineQuery
@ -16,12 +17,9 @@ import dev.inmo.tgbotapi.extensions.utils.types.buttons.inlineKeyboard
import dev.inmo.tgbotapi.extensions.utils.types.buttons.replyKeyboard import dev.inmo.tgbotapi.extensions.utils.types.buttons.replyKeyboard
import dev.inmo.tgbotapi.extensions.utils.types.buttons.webAppButton import dev.inmo.tgbotapi.extensions.utils.types.buttons.webAppButton
import dev.inmo.tgbotapi.requests.answers.InlineQueryResultsButton import dev.inmo.tgbotapi.requests.answers.InlineQueryResultsButton
import dev.inmo.tgbotapi.types.BotCommand import dev.inmo.tgbotapi.types.*
import dev.inmo.tgbotapi.types.InlineQueries.InlineQueryResult.InlineQueryResultArticle import dev.inmo.tgbotapi.types.InlineQueries.InlineQueryResult.InlineQueryResultArticle
import dev.inmo.tgbotapi.types.InlineQueries.InputMessageContent.InputTextMessageContent import dev.inmo.tgbotapi.types.InlineQueries.InputMessageContent.InputTextMessageContent
import dev.inmo.tgbotapi.types.InlineQueryId
import dev.inmo.tgbotapi.types.LinkPreviewOptions
import dev.inmo.tgbotapi.types.webAppQueryIdField
import dev.inmo.tgbotapi.types.webapps.WebAppInfo import dev.inmo.tgbotapi.types.webapps.WebAppInfo
import dev.inmo.tgbotapi.utils.* import dev.inmo.tgbotapi.utils.*
import io.ktor.http.* import io.ktor.http.*
@ -30,7 +28,6 @@ import io.ktor.server.http.content.*
import io.ktor.server.request.* import io.ktor.server.request.*
import io.ktor.server.response.* import io.ktor.server.response.*
import io.ktor.server.routing.* import io.ktor.server.routing.*
import kotlinx.coroutines.Dispatchers
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import java.io.File import java.io.File
@ -105,6 +102,26 @@ suspend fun main(vararg args: String) {
call.respond(HttpStatusCode.OK, isSafe.toString()) call.respond(HttpStatusCode.OK, isSafe.toString())
} }
post("setCustomEmoji") {
val requestBody = call.receiveText()
val webAppCheckData = Json.decodeFromString(WebAppDataWrapper.serializer(), requestBody)
val isSafe = telegramBotAPIUrlsKeeper.checkWebAppData(webAppCheckData.data, webAppCheckData.hash)
val rawUserId = call.parameters[userIdField] ?.toLongOrNull() ?.let(::RawChatId) ?: error("$userIdField should be presented as long value")
val set = if (isSafe) {
runCatching {
bot.setUserEmojiStatus(
UserId(rawUserId),
CustomEmojiIdToSet
)
}.getOrElse { false }
} else {
false
}
call.respond(HttpStatusCode.OK, set.toString())
}
} }
}.start(false) }.start(false)

View File

@ -29,3 +29,8 @@ allprojects {
maven { url "https://nexus.inmo.dev/repository/maven-releases/" } maven { url "https://nexus.inmo.dev/repository/maven-releases/" }
} }
} }
// Fix of https://youtrack.jetbrains.com/issue/KTOR-7912/Module-not-found-errors-when-executing-browserProductionWebpack-task-since-3.0.2
rootProject.plugins.withType(org.jetbrains.kotlin.gradle.targets.js.yarn.YarnPlugin.class) {
rootProject.kotlinYarn.resolution("ws", "8.18.0")
}

View File

@ -6,7 +6,8 @@ kotlin.daemon.jvmargs=-Xmx3g -Xms500m
kotlin_version=2.1.0 kotlin_version=2.1.0
telegram_bot_api_version=21.0.0 telegram_bot_api_version=22.0.0
micro_utils_version=0.23.1 micro_utils_version=0.23.2
serialization_version=1.7.3 serialization_version=1.7.3
ktor_version=3.0.1 ktor_version=3.0.2
compose_version=1.7.1