Browse Source

added basic weather bot implementation

Vadik Sirekanyan 5 năm trước cách đây
mục cha
commit
1ca6f0f255

+ 4 - 0
.gitignore

@@ -0,0 +1,4 @@
+/.gradle
+/.idea
+/build
+/bot.properties

+ 42 - 0
src/main/kotlin/com/sirekanyan/andersrobot/AndersRobot.kt

@@ -0,0 +1,42 @@
+package com.sirekanyan.andersrobot
+
+import com.sirekanyan.andersrobot.api.WeatherApi
+import com.sirekanyan.andersrobot.config.Config
+import com.sirekanyan.andersrobot.config.ConfigKey.BOT_TOKEN
+import com.sirekanyan.andersrobot.config.ConfigKey.BOT_USERNAME
+import com.sirekanyan.andersrobot.extensions.isWeatherCommand
+import com.sirekanyan.andersrobot.extensions.sendText
+import org.telegram.telegrambots.bots.DefaultAbsSender
+import org.telegram.telegrambots.bots.DefaultBotOptions
+import org.telegram.telegrambots.meta.api.objects.Update
+import org.telegram.telegrambots.meta.generics.LongPollingBot
+import org.telegram.telegrambots.util.WebhookUtils
+
+val botName = Config[BOT_USERNAME]
+
+class AndersRobot : DefaultAbsSender(DefaultBotOptions()), LongPollingBot {
+
+    private val weather = WeatherApi()
+
+    override fun getBotUsername(): String = botName
+
+    override fun getBotToken(): String = Config[BOT_TOKEN]
+
+    override fun onUpdateReceived(update: Update) {
+        val message = update.message
+        val chatId = message.chatId
+        when {
+            isWeatherCommand(message.text) -> {
+                val temperatures = weather.getTemperatures()
+                if (temperatures.isNotEmpty()) {
+                    sendText(chatId, temperatures.joinToString("\n"))
+                }
+            }
+        }
+    }
+
+    override fun clearWebhook() {
+        WebhookUtils.clearWebhook(this)
+    }
+
+}

+ 4 - 1
src/main/kotlin/com/sirekanyan/andersrobot/api/Weather.kt

@@ -3,9 +3,12 @@ package com.sirekanyan.andersrobot.api
 import kotlinx.serialization.Serializable
 
 @Serializable
-data class Weather(val main: MainInfo) {
+data class Weather(val main: MainInfo, val name: String, val sys: Sys) {
 
     @Serializable
     data class MainInfo(val temp: Double)
 
+    @Serializable
+    data class Sys(val country: String)
+
 }

+ 64 - 0
src/main/kotlin/com/sirekanyan/andersrobot/api/WeatherApi.kt

@@ -0,0 +1,64 @@
+package com.sirekanyan.andersrobot.api
+
+import com.sirekanyan.andersrobot.config.Config
+import com.sirekanyan.andersrobot.config.ConfigKey.WEATHER_API_KEY
+import io.ktor.client.*
+import io.ktor.client.request.*
+import kotlinx.coroutines.async
+import kotlinx.coroutines.runBlocking
+import kotlinx.serialization.decodeFromString
+import kotlinx.serialization.json.Json
+
+private const val WEATHER_URL = "https://api.openweathermap.org/data/2.5/weather"
+private val CITIES = listOf(
+    "Miass",
+    "Chelyabinsk",
+    "Moscow",
+    "Yerevan",
+    "Boston",
+)
+
+class WeatherApi {
+
+    private val httpClient = HttpClient()
+    private val apiKey = Config[WEATHER_API_KEY]
+
+    fun getTemperature(city: String, accuracy: Int): String? = runBlocking {
+        val weather = getWeather(city)
+        weather?.let {
+            formatWeather(weather, accuracy) + " — " + formatCity(weather)
+        }
+    }
+
+    fun getTemperatures(accuracy: Int): List<String> = runBlocking {
+        CITIES.associateWith { city ->
+            async { getWeather(city) }
+        }.map { (city, weather) ->
+            weather.await()?.let {
+                formatWeather(it, accuracy) + " — $city"
+            }
+        }.filterNotNull()
+    }
+
+    private suspend fun getWeather(city: String): Weather? =
+        try {
+            println("getting $city")
+            val response: String = httpClient.get(WEATHER_URL) {
+                parameter("q", city)
+                parameter("units", "metric")
+                parameter("appid", apiKey)
+            }
+            val json = Json { ignoreUnknownKeys = true }
+            json.decodeFromString(response)
+        } catch (ex: Exception) {
+            ex.printStackTrace()
+            null
+        }
+
+    private fun formatWeather(weather: Weather, accuracy: Int): String =
+        "%.${accuracy}f°C".format(weather.main.temp)
+
+    private fun formatCity(weather: Weather): String =
+        "${weather.name} (${weather.sys.country})"
+
+}

+ 41 - 0
src/main/kotlin/com/sirekanyan/andersrobot/config/Config.kt

@@ -0,0 +1,41 @@
+package com.sirekanyan.andersrobot.config
+
+import java.io.File
+import java.util.*
+
+private const val CONFIG_FILE = "bot.properties"
+
+object Config {
+
+    private val properties = initProperties(initFile())
+
+    private fun initFile(): File {
+        val file = File(CONFIG_FILE)
+        if (!file.exists()) {
+            file.bufferedWriter().use { writer ->
+                ConfigKey.values().forEach { key ->
+                    writer.appendLine("${key.name}=")
+                }
+            }
+        }
+        return file
+    }
+
+    private fun initProperties(file: File): Properties {
+        val properties = Properties()
+        file.inputStream().use { configFile ->
+            properties.load(configFile)
+        }
+        val missingProperties = ConfigKey.values().filter { key ->
+            properties.getProperty(key.toString()).isNullOrBlank()
+        }
+        if (missingProperties.any()) {
+            throw IllegalArgumentException("Missing $missingProperties in $CONFIG_FILE file")
+        }
+        return properties
+    }
+
+    operator fun get(key: ConfigKey): String =
+        properties.getProperty(key.toString())
+
+}

+ 8 - 0
src/main/kotlin/com/sirekanyan/andersrobot/config/ConfigKey.kt

@@ -0,0 +1,8 @@
+package com.sirekanyan.andersrobot.config
+
+enum class ConfigKey {
+    BOT_USERNAME,
+    BOT_TOKEN,
+    ADMIN_ID,
+    WEATHER_API_KEY,
+}

+ 8 - 0
src/main/kotlin/com/sirekanyan/andersrobot/extensions/AbsSender.kt

@@ -0,0 +1,8 @@
+package com.sirekanyan.andersrobot.extensions
+
+import org.telegram.telegrambots.meta.api.methods.send.SendMessage
+import org.telegram.telegrambots.meta.api.objects.Message
+import org.telegram.telegrambots.meta.bots.AbsSender
+
+fun AbsSender.sendText(chatId: Long, text: String): Message =
+    execute(SendMessage(chatId, text))

+ 9 - 0
src/main/kotlin/com/sirekanyan/andersrobot/extensions/String.kt

@@ -0,0 +1,9 @@
+package com.sirekanyan.andersrobot.extensions
+
+import com.sirekanyan.andersrobot.botName
+import kotlin.text.RegexOption.IGNORE_CASE
+
+private val REGEX = Regex("\\b(андерс|anders|погод[аеуы])\\b", IGNORE_CASE)
+
+fun isWeatherCommand(text: String?): Boolean =
+    text?.contains(REGEX) == true || text == "/temp" || text == "/temp@$botName"