diff --git a/.idea/artifacts/moztw_space_bot_jar.xml b/.idea/artifacts/moztw_space_bot_jar.xml index 6b8304d..5b6a718 100644 --- a/.idea/artifacts/moztw_space_bot_jar.xml +++ b/.idea/artifacts/moztw_space_bot_jar.xml @@ -2,55 +2,62 @@ $PROJECT_DIR$/out/artifacts/moztw_space_bot_jar - - - - - + - - - - - - - - - - - + + - - - - - - + + - + + + + + + + + - - - + + + + - - + + + - - - - - + - + + - + + + + + + + + - - + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules/moztw-space-bot.main.iml b/.idea/modules/moztw-space-bot.main.iml index 40b7226..02f6461 100644 --- a/.idea/modules/moztw-space-bot.main.iml +++ b/.idea/modules/moztw-space-bot.main.iml @@ -2,23 +2,34 @@ - - + + + - @@ -34,11 +45,15 @@ - + - - + + + + + + @@ -52,6 +67,9 @@ + + + diff --git a/.idea/modules/moztw-space-bot.test.iml b/.idea/modules/moztw-space-bot.test.iml index f6bf55d..5015084 100644 --- a/.idea/modules/moztw-space-bot.test.iml +++ b/.idea/modules/moztw-space-bot.test.iml @@ -2,23 +2,40 @@ - - + + + + - @@ -33,12 +50,16 @@ - + + + - - + + + + @@ -52,9 +73,12 @@ + + + diff --git a/build.gradle.kts b/build.gradle.kts index 412b465..8fa3ab1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,7 +2,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile buildscript { var kotlin_version: String by extra - kotlin_version = "1.2.30" + kotlin_version = "1.3.50" repositories { mavenCentral() @@ -27,12 +27,15 @@ val testImplementation by configurations repositories { mavenCentral() + jcenter() } dependencies { implementation(kotlin("stdlib-jdk8", kotlin_version)) implementation("commons-cli", "commons-cli", "1.4") implementation("org.telegram", "telegrambots", "3.6") + implementation("com.squareup.okhttp3", "okhttp", "4.2.1") + implementation("com.beust", "klaxon", "5.0.13") testImplementation("org.junit.jupiter", "junit-jupiter-api", "5.1.0") } diff --git a/src/main/kotlin/org/moztw/bot/telegram/space/Bot.kt b/src/main/kotlin/org/moztw/bot/telegram/space/Bot.kt index 02f6129..bd6665f 100644 --- a/src/main/kotlin/org/moztw/bot/telegram/space/Bot.kt +++ b/src/main/kotlin/org/moztw/bot/telegram/space/Bot.kt @@ -1,7 +1,13 @@ package org.moztw.bot.telegram.space +import okhttp3.OkHttpClient +import okhttp3.Request +import org.moztw.bot.telegram.space.co2.SpaceCo2 +import org.telegram.telegrambots.api.methods.ActionType import org.telegram.telegrambots.api.methods.groupadministration.SetChatTitle +import org.telegram.telegrambots.api.methods.send.SendChatAction import org.telegram.telegrambots.api.methods.send.SendMessage +import org.telegram.telegrambots.api.methods.send.SendPhoto import org.telegram.telegrambots.api.objects.CallbackQuery import org.telegram.telegrambots.api.objects.Chat import org.telegram.telegrambots.api.objects.Message @@ -9,6 +15,9 @@ import org.telegram.telegrambots.api.objects.Update import org.telegram.telegrambots.bots.TelegramLongPollingBot import org.telegram.telegrambots.exceptions.TelegramApiException import java.text.SimpleDateFormat +import java.time.ZoneId +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter import java.util.* internal class Bot(val username: String, val token: String) : TelegramLongPollingBot() { @@ -32,26 +41,82 @@ internal class Bot(val username: String, val token: String) : TelegramLongPollin } private fun onMessageReceived(message: Message): Boolean { - if (isAdminChat(message.chat) && message.hasText()) { - if (isCommandOpen(message.text)) { - if (tryExecute(Caption().getCaptionOpened(chatId = generalChatId)) - && tryExecute(Reply().getGeneralMessageOpen(chatId = generalChatId, operator = message.from)) - && tryExecute(Reply().getMessageOpen(message = message))) { - for (chatId in adminChats) - if (chatId != message.chatId) - if (!tryExecute(Reply().getOtherMessageOpen(message = message, chatId = chatId, operator = message.from))) - return false - return true + if (message.hasText()) { + if (isCommandCO2(message.text)) { + try { + execute(SendChatAction().apply { + chatId = message.chat.id.toString() + action = ActionType.TYPING + }) + } catch (e: TelegramApiException) { + e.printStackTrace() } - } else if (isCommandClose(message.text)) { - if (tryExecute(Caption().getCaptionClosed(chatId = generalChatId)) - && tryExecute(Reply().getGeneralMessageClose(chatId = generalChatId, operator = message.from)) - && tryExecute(Reply().getMessageClose(message = message))) { - for (chatId in adminChats) - if (chatId != message.chatId) - if (!tryExecute(Reply().getOtherMessageClose(message = message, chatId = chatId, operator = message.from))) - return false - return true + + var messageText = "二氧化碳含量取得失敗,本服務暫時無法使用。" + var imageUrl = "" + try { + SpaceCo2().fetchCo2()?.let { co2 -> + val last = co2.feeds.last() + val lastUpdate = ZonedDateTime + .parse(last.created_at) + .withZoneSameInstant(ZoneId.of("Asia/Taipei")) + .format(DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss")) + messageText = "目前的二氧化碳含量為 ${last.field1} ppm\n" + + "更新日期:$lastUpdate" + imageUrl = SpaceCo2().chartPng(co2) + } + } finally { + if (imageUrl.isEmpty()) { + tryExecute(SendMessage().apply { + chatId = message.chat.id.toString() + replyToMessageId = message.messageId + text = messageText + setParseMode("HTML") + }) + } else { + try { + execute(SendChatAction().apply { + chatId = message.chat.id.toString() + action = ActionType.UPLOADPHOTO + }) + + sendPhoto(SendPhoto().apply { + caption = messageText + chatId = message.chat.id.toString() + parseMode = "HTML" + replyToMessageId = message.messageId + setNewPhoto("moztw-space-co2.png", imageUrl.let { + OkHttpClient() + .newCall(Request.Builder().url(it).build()) + .execute().body?.byteStream() + }) + }) + } catch (e: TelegramApiException) { + e.printStackTrace() + } + } + } + } else if (isAdminChat(message.chat)) { + if (isCommandOpen(message.text)) { + if (tryExecute(Caption().getCaptionOpened(chatId = generalChatId)) + && tryExecute(Reply().getGeneralMessageOpen(chatId = generalChatId, operator = message.from)) + && tryExecute(Reply().getMessageOpen(message = message))) { + for (chatId in adminChats) + if (chatId != message.chatId) + if (!tryExecute(Reply().getOtherMessageOpen(message = message, chatId = chatId, operator = message.from))) + return false + return true + } + } else if (isCommandClose(message.text)) { + if (tryExecute(Caption().getCaptionClosed(chatId = generalChatId)) + && tryExecute(Reply().getGeneralMessageClose(chatId = generalChatId, operator = message.from)) + && tryExecute(Reply().getMessageClose(message = message))) { + for (chatId in adminChats) + if (chatId != message.chatId) + if (!tryExecute(Reply().getOtherMessageClose(message = message, chatId = chatId, operator = message.from))) + return false + return true + } } } } @@ -89,6 +154,10 @@ internal class Bot(val username: String, val token: String) : TelegramLongPollin return text.matches("^/space_open(?:\\s|$)".toRegex()) || text.matches("^/space_open@$botUsername(?:\\s|$)".toRegex()) } + private fun isCommandCO2(text: String): Boolean { + return text.matches("^/co2(?:\\s|$)".toRegex()) || text.matches("^/co2@$botUsername(?:\\s|$)".toRegex()) + } + private fun isAdminChat(chat: Chat) = adminChats.contains(chat.id) override fun getBotUsername() = username diff --git a/src/main/kotlin/org/moztw/bot/telegram/space/co2/SpaceCo2.kt b/src/main/kotlin/org/moztw/bot/telegram/space/co2/SpaceCo2.kt new file mode 100644 index 0000000..ede1731 --- /dev/null +++ b/src/main/kotlin/org/moztw/bot/telegram/space/co2/SpaceCo2.kt @@ -0,0 +1,23 @@ +package org.moztw.bot.telegram.space.co2 + +import com.beust.klaxon.Klaxon +import okhttp3.OkHttpClient +import okhttp3.Request +import org.moztw.bot.telegram.space.co2.model.Chart +import org.moztw.bot.telegram.space.co2.model.Co2 +import java.io.IOException +import java.time.ZoneId +import java.time.ZonedDateTime + +class SpaceCo2 { + private val url = "https://thingspeak.com/channels/631210/feed.json" + + @Throws(IOException::class) + fun fetchCo2() = OkHttpClient() + .newCall(Request.Builder().url(url).build()) + .execute().body?.string()?.let { json -> Klaxon().parse(json) } + + fun chartPng(co2: Co2) = Chart(co2.feeds.map { + Pair(ZonedDateTime.parse(it.created_at).withZoneSameInstant(ZoneId.of("Asia/Taipei")), it.field1) + }.toList()).url +} diff --git a/src/main/kotlin/org/moztw/bot/telegram/space/co2/model/Channel.kt b/src/main/kotlin/org/moztw/bot/telegram/space/co2/model/Channel.kt new file mode 100644 index 0000000..6d006e3 --- /dev/null +++ b/src/main/kotlin/org/moztw/bot/telegram/space/co2/model/Channel.kt @@ -0,0 +1,13 @@ +package org.moztw.bot.telegram.space.co2.model + +data class Channel( + val id: Int, + val name: String, + val description: String, + val latitude: String, + val longitude: String, + val field1: String, + val created_at: String, + val updated_at: String, + val last_entry_id: Int +) diff --git a/src/main/kotlin/org/moztw/bot/telegram/space/co2/model/Chart.kt b/src/main/kotlin/org/moztw/bot/telegram/space/co2/model/Chart.kt new file mode 100644 index 0000000..047b9a4 --- /dev/null +++ b/src/main/kotlin/org/moztw/bot/telegram/space/co2/model/Chart.kt @@ -0,0 +1,41 @@ +package org.moztw.bot.telegram.space.co2.model + +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter +import javax.ws.rs.core.UriBuilder + +data class Chart( + val data: List> +) { + private val dataX = data.map { it.first.format(DateTimeFormatter.ofPattern("HH:mm")) } + private val dataY = data.map { it.second.toInt() } + + private val dataMin = dataY.minBy { it } ?: 0 + private val dataMax = dataY.maxBy { it } ?: 0 + private val dataMid = (dataMin + dataMax) / 2 + private val dataQuad = arrayOf(dataMin, (dataMin + dataMid) / 2, dataMid, (dataMid + dataMax) / 2, dataMax) + + private val chartData = dataY.joinToString(",", "t:") + private val chartDataScale = "$dataMin,$dataMax" + private val chartGrid = "10,25" + private val chartLabel = dataX.filterIndexed { index, _ -> 99 == index || 0 == (index % 10) }.joinToString("|") + private val chartSize = "640x360" + private val chartType = "lc" + private val chartTitle = "Concentration of CO2 in Mozilla Community Space Taipei" + private val chartAxisLabel = "1:|${dataQuad.joinToString("|") { "$it ppm" }}" + private val chartAxis = "x,y" + + val url = UriBuilder + .fromPath("https://chart.googleapis.com/chart") + .queryParam("chd", chartData) + .queryParam("chds", chartDataScale) + .queryParam("chg", chartGrid) + .queryParam("chl", chartLabel) + .queryParam("chs", chartSize) + .queryParam("cht", chartType) + .queryParam("chtt", chartTitle) + .queryParam("chxl", chartAxisLabel) + .queryParam("chxt", chartAxis) + .build() + .toASCIIString()!! +} diff --git a/src/main/kotlin/org/moztw/bot/telegram/space/co2/model/Co2.kt b/src/main/kotlin/org/moztw/bot/telegram/space/co2/model/Co2.kt new file mode 100644 index 0000000..4ce3b4b --- /dev/null +++ b/src/main/kotlin/org/moztw/bot/telegram/space/co2/model/Co2.kt @@ -0,0 +1,24 @@ +package org.moztw.bot.telegram.space.co2.model + +data class Co2( + val channel: Channel, + val feeds: Array +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Co2 + + if (channel != other.channel) return false + if (!feeds.contentEquals(other.feeds)) return false + + return true + } + + override fun hashCode(): Int { + var result = channel.hashCode() + result = 31 * result + feeds.contentHashCode() + return result + } +} diff --git a/src/main/kotlin/org/moztw/bot/telegram/space/co2/model/Feed.kt b/src/main/kotlin/org/moztw/bot/telegram/space/co2/model/Feed.kt new file mode 100644 index 0000000..653c8a8 --- /dev/null +++ b/src/main/kotlin/org/moztw/bot/telegram/space/co2/model/Feed.kt @@ -0,0 +1,7 @@ +package org.moztw.bot.telegram.space.co2.model + +data class Feed( + val created_at: String, + val entry_id: Int, + val field1: String +)