diff --git a/WebApp/build.gradle b/WebApp/build.gradle index 53e310e..8e04927 100644 --- a/WebApp/build.gradle +++ b/WebApp/build.gradle @@ -11,6 +11,9 @@ buildscript { plugins { id "org.jetbrains.kotlin.multiplatform" 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' @@ -27,12 +30,15 @@ kotlin { dependencies { implementation kotlin('stdlib') implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$serialization_version" + implementation "dev.inmo:tgbotapi.core:$telegram_bot_api_version" + implementation compose.runtime } } jsMain { dependencies { 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:micro_utils.ktor.server:$micro_utils_version" implementation "io.ktor:ktor-server-cio:$ktor_version" + implementation compose.desktop.currentOs } } } diff --git a/WebApp/src/commonMain/kotlin/CustomEmojiIdToSet.kt b/WebApp/src/commonMain/kotlin/CustomEmojiIdToSet.kt new file mode 100644 index 0000000..6c384a6 --- /dev/null +++ b/WebApp/src/commonMain/kotlin/CustomEmojiIdToSet.kt @@ -0,0 +1,3 @@ +import dev.inmo.tgbotapi.types.CustomEmojiId + +val CustomEmojiIdToSet = CustomEmojiId("5424939566278649034") diff --git a/WebApp/src/jsMain/kotlin/main.kt b/WebApp/src/jsMain/kotlin/main.kt index 6802855..fa1c3d0 100644 --- a/WebApp/src/jsMain/kotlin/main.kt +++ b/WebApp/src/jsMain/kotlin/main.kt @@ -1,22 +1,35 @@ +import androidx.compose.runtime.* 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.webapps.* +import dev.inmo.tgbotapi.webapps.accelerometer.AccelerometerStartParams 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.HapticFeedbackType +import dev.inmo.tgbotapi.webapps.orientation.DeviceOrientationStartParams import dev.inmo.tgbotapi.webapps.popup.* import io.ktor.client.HttpClient import io.ktor.client.request.* import io.ktor.client.statement.bodyAsText import io.ktor.http.* import io.ktor.http.content.TextContent -import kotlinx.browser.document import kotlinx.browser.window import kotlinx.coroutines.* import kotlinx.dom.appendElement import kotlinx.dom.appendText -import kotlinx.dom.clear 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 kotlin.random.Random import kotlin.random.nextUBytes @@ -32,277 +45,356 @@ fun main() { val client = HttpClient() val baseUrl = window.location.origin.removeSuffix("/") - window.onload = { - val scope = CoroutineScope(Dispatchers.Default) - runCatching { + renderComposable("root") { + val scope = rememberCoroutineScope() + val isSafeState = remember { mutableStateOf(null) } + val logsState = remember { mutableStateListOf() } - scope.launchSafelyWithoutExceptions { - val response = client.post("$baseUrl/check") { - setBody( - Json.encodeToString( - WebAppDataWrapper.serializer(), - WebAppDataWrapper(webApp.initData, webApp.initDataUnsafe.hash) - ) +// Text(window.location.href) +// P() + + LaunchedEffect(baseUrl) { + val response = client.post("$baseUrl/check") { + 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") { - addEventListener("click", { - scope.launchSafelyWithoutExceptions { - handleResult({ "Clicked" }) { - client.post("${window.location.origin.removeSuffix("/")}/inline") { - parameter(webAppQueryIdField, it) - setBody(TextContent("Clicked", ContentType.Text.Plain)) - document.body ?.log(url.build().toString()) - }.coroutineContext.job.join() - } - } - }) - 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" - } + }) { + Text("Popup") + } + Button({ + onClick { + webApp.showAlert( + "This is alert message" + ) { + logsState.add( + "You have closed alert" + ) } - addEventListener("click", { - webApp.toggleClosingConfirmation() - updateText() - }) - updateText() - } ?: window.alert("Unable to load body") + } + }) { + Text("Alert") + } - document.body ?.appendElement("p", {}) - - document.body ?.appendElement("button") { - fun updateHeaderColor() { - val (r, g, b) = Random.nextUBytes(3) - val hex = Color.Hex(r, g, b) - webApp.setHeaderColor(hex) - (this as? HTMLButtonElement) ?.style ?.backgroundColor = hex.value - textContent = "Header color: ${webApp.headerColor ?.uppercase()} (click to change)" + P() + Button({ + onClick { + webApp.requestWriteAccess() + } + }) { + Text("Request write access without callback") + } + 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("#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() - } ?: 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") { - fun updateBackgroundColor() { - val (r, g, b) = Random.nextUBytes(3) - val hex = Color.Hex(r, g, b) - webApp.setBackgroundColor(hex) - (this as? HTMLButtonElement) ?.style ?.backgroundColor = hex.value - textContent = "Background color: ${webApp.backgroundColor ?.uppercase()} (click to change)" - } - addEventListener("click", { - updateBackgroundColor() - }) + val backgroundColor = remember { mutableStateOf(Color.Hex("#000000")) } + fun updateBackgroundColor() { + val (r, g, b) = Random.nextUBytes(3) + backgroundColor.value = Color.Hex(r, g, b) + webApp.setBackgroundColor(backgroundColor.value) + } + DisposableEffect(0) { + updateBackgroundColor() + onDispose { } + } + Button({ + style { + backgroundColor(ComposeColor(backgroundColor.value.value)) + } + onClick { 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") { - fun updateBottomBarColor() { - val (r, g, b) = Random.nextUBytes(3) - val hex = Color.Hex(r, g, b) - webApp.setBottomBarColor(hex) - (this as? HTMLButtonElement) ?.style ?.backgroundColor = hex.value - textContent = "Bottom bar color: ${webApp.bottomBarColor ?.uppercase()} (click to change)" - } - addEventListener("click", { - updateBottomBarColor() - }) + val bottomBarColor = remember { mutableStateOf(Color.Hex("#000000")) } + fun updateBottomBarColor() { + val (r, g, b) = Random.nextUBytes(3) + bottomBarColor.value = Color.Hex(r, g, b) + webApp.setBottomBarColor(bottomBarColor.value) + } + DisposableEffect(0) { + updateBottomBarColor() + onDispose { } + } + Button({ + style { + backgroundColor(ComposeColor(bottomBarColor.value.value)) + } + onClick { 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() { - clear() - webApp.cloudStorage.getAll { - it.onSuccess { - document.body ?.log(it.toString()) - 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 storageTrigger = remember { mutableStateOf>>(emptyList()) } + fun updateCloudStorage() { + webApp.cloudStorage.getAll { + it.onSuccess { + storageTrigger.value = it.toList().sortedBy { it.first.key } } } - val cloudStorageContentDiv = document.body ?.appendElement("div") {} as HTMLDivElement - - document.body ?.appendElement("p", {}) + } + key(storageTrigger.value) { + 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 { + onThemeChanged { - document.body ?.log("Theme changed: ${webApp.themeParams}") + logsState.add("Theme changed: ${webApp.themeParams}") } onViewportChanged { - document.body ?.log("Viewport changed: ${it.isStateStable}") + logsState.add("Viewport changed: ${it}") } backButton.apply { onClick { - document.body ?.log("Back button clicked") + logsState.add("Back button clicked") hapticFeedback.impactOccurred( HapticFeedbackStyle.Heavy ) @@ -312,7 +404,7 @@ fun main() { mainButton.apply { setText("Main button") onClick { - document.body ?.log("Main button clicked") + logsState.add("Main button clicked") hapticFeedback.notificationOccurred( HapticFeedbackType.Success ) @@ -322,7 +414,7 @@ fun main() { secondaryButton.apply { setText("Secondary button") onClick { - document.body ?.log("Secondary button clicked") + logsState.add("Secondary button clicked") hapticFeedback.notificationOccurred( HapticFeedbackType.Warning ) @@ -330,22 +422,231 @@ fun main() { show() } onSettingsButtonClicked { - document.body ?.log("Settings button clicked") + logsState.add("Settings button clicked") } onWriteAccessRequested { - document.body ?.log("Write access request result: $it") + logsState.add("Write access request result: $it") } onContactRequested { - document.body ?.log("Contact request result: $it") + logsState.add("Contact request result: $it") } } - webApp.ready() - document.body ?.appendElement("input", { - (this as HTMLInputElement).value = window.location.href - }) - cloudStorageContentDiv.updateCloudStorageContent() - }.onFailure { - window.alert(it.stackTraceToString()) + } + P() + + let { // Accelerometer + val enabledState = remember { mutableStateOf(webApp.accelerometer.isStarted) } + webApp.onAccelerometerStarted { enabledState.value = true } + 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()) } } } } diff --git a/WebApp/src/jsMain/resources/index.html b/WebApp/src/jsMain/resources/index.html index ab9d94d..7d253ac 100644 --- a/WebApp/src/jsMain/resources/index.html +++ b/WebApp/src/jsMain/resources/index.html @@ -10,6 +10,7 @@ Web App Example +
diff --git a/WebApp/src/jvmMain/kotlin/WebAppServer.kt b/WebApp/src/jvmMain/kotlin/WebAppServer.kt index f067cd2..d6ed35f 100644 --- a/WebApp/src/jvmMain/kotlin/WebAppServer.kt +++ b/WebApp/src/jvmMain/kotlin/WebAppServer.kt @@ -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.send.reply 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.behaviour_builder.buildBehaviourWithLongPolling 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.webAppButton 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.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.utils.* import io.ktor.http.* @@ -30,7 +28,6 @@ import io.ktor.server.http.content.* import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.routing.* -import kotlinx.coroutines.Dispatchers import kotlinx.serialization.json.Json import java.io.File @@ -105,6 +102,26 @@ suspend fun main(vararg args: String) { 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) diff --git a/build.gradle b/build.gradle index 40b825e..ddbaec4 100644 --- a/build.gradle +++ b/build.gradle @@ -29,3 +29,8 @@ allprojects { 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") +} diff --git a/gradle.properties b/gradle.properties index 381249d..df2afb2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,7 +6,8 @@ kotlin.daemon.jvmargs=-Xmx3g -Xms500m kotlin_version=2.1.0 -telegram_bot_api_version=21.0.0 -micro_utils_version=0.23.1 +telegram_bot_api_version=22.0.0 +micro_utils_version=0.23.2 serialization_version=1.7.3 -ktor_version=3.0.1 +ktor_version=3.0.2 +compose_version=1.7.1