package com.chameth.hexbot.modules import com.chameth.hexbot.isModuleCommand import com.chameth.hexbot.moduleCommandArgs import com.dmdirc.ktirc.IrcClient import com.dmdirc.ktirc.events.IrcEvent import com.dmdirc.ktirc.events.MessageReceived import com.dmdirc.ktirc.events.react import com.dmdirc.ktirc.events.reply import com.google.gson.annotations.SerializedName import com.ufoscout.properlty.Properlty import io.ktor.client.HttpClient import io.ktor.client.engine.okhttp.OkHttp import io.ktor.client.features.json.GsonSerializer import io.ktor.client.features.json.JsonFeature import io.ktor.client.request.* import org.jetbrains.exposed.sql.SchemaUtils import org.jetbrains.exposed.sql.Table import org.jetbrains.exposed.sql.insert import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.transactions.transaction import org.joda.time.DateTime import java.sql.Connection import java.util.* import kotlin.contracts.ExperimentalContracts class Hue : Module { private var appId: String? = null private var clientId: String? = null private var clientSecret: String? = null private val basicHeader by lazy { Base64.getEncoder().encodeToString("$clientId:$clientSecret".toByteArray()) } private val httpClient = HttpClient(OkHttp) { engine { config { followRedirects(true) } } install(JsonFeature) { serializer = GsonSerializer() } } override fun init(settings: Properlty): Boolean { clientId = settings["hue.clientid", null] clientSecret = settings["hue.clientsecret", null] appId = settings["hue.appid", null] if (clientId == null || clientSecret == null || appId == null) { return false } transaction(Connection.TRANSACTION_SERIALIZABLE, 1) { SchemaUtils.create(HueRecords) } return true } @ExperimentalContracts override suspend fun processEvent(client: IrcClient, event: IrcEvent) { if (client.isModuleCommand(event, "hue")) { val account = event.user.account ?: client.userState[event.user]?.details?.account if (account == null) { client.reply(event, "You must be authenticated to use this command", true) return } val args = event.moduleCommandArgs when (args.firstOrNull()) { "auth" -> handleAuth(args.getOrNull(1), account) { client.reply(event, it, true) } "list" -> handleList(account) { client.reply(event, it, true) } "turn" -> handleTurn(account, args[1].toInt(), args[2] == "on", client, event) } } } private suspend fun handleTurn(account: String, id: Int, state: Boolean, ircClient: IrcClient, event: MessageReceived) { lateinit var token: String lateinit var username: String // TODO: This continues with HTTP request even if the record is empty transaction(Connection.TRANSACTION_SERIALIZABLE, 1) { val records = HueRecords.select { HueRecords.userName eq account } if (records.empty()) { ircClient.reply(event, "You do not seem to have set up Hue. Use the 'hue auth' command to get started.") return@transaction } val record = records.first() token = record[HueRecords.accessToken] username = record[HueRecords.bridgeUsername] } try { httpClient.put("https://api.meethue.com/bridge/$username/lights/$id/state") { header("Authorization", "Bearer $token") header("Content-type", "application/json") body = mapOf("on" to state) } ircClient.react(event, "💡 ${if (state) "On" else "Off"}") } catch (ex : Exception) { ircClient.reply(event, "+++ Unable to complete action. Please Reinstall Universe And Reboot. +++") } } private suspend fun handleList(account: String, replier: (String) -> Unit) { lateinit var token: String lateinit var username: String transaction(Connection.TRANSACTION_SERIALIZABLE, 1) { val records = HueRecords.select { HueRecords.userName eq account } if (records.empty()) { replier("You do not seem to have set up Hue. Use the 'hue auth' command to get started.") return@transaction } val record = records.first() token = record[HueRecords.accessToken] username = record[HueRecords.bridgeUsername] } val lights = httpClient.get>("https://api.meethue.com/bridge/$username/lights") { header("Authorization", "Bearer $token") } lights.forEach { id, light -> replier("Light $id is a ${light.manufacturerName} ${light.productName} named ${light.name}") } } private suspend fun handleAuth(arg: String?, account: String, replier: (String) -> Unit) { when (arg) { null -> replier("Please go to https://api.meethue.com/oauth2/auth?clientid=$clientId&appid=$appId&deviceid=IRC&devicename=irc&state=hexbot&response_type=code and send me the code in another `auth` command") else -> try { val response = httpClient.post("https://api.meethue.com/oauth2/token?code=$arg&grant_type=authorization_code") { body = "" headers { set("Authorization", "Basic $basicHeader") } } httpClient.put("https://api.meethue.com/bridge/0/config") { headers { set("Authorization", "Bearer ${response.accessToken}") set("Content-Type", "application/json") } body = mapOf("linkbutton" to true) } val username = httpClient.post>>>("https://api.meethue.com/bridge/") { headers { set("Authorization", "Bearer ${response.accessToken}") set("Content-Type", "application/json") } body = mapOf("devicetype" to "hexbot") }[0]["success"]?.getValue("username")!! transaction(Connection.TRANSACTION_SERIALIZABLE, 1) { HueRecords.insert { it[userName] = account it[accessToken] = response.accessToken!! it[accessTokenExpiry] = DateTime.now().plusSeconds(response.accessTokenExpiry!!.toInt()) it[refreshToken] = response.refreshToken!! it[refreshTokenExpiry] = DateTime.now().plusSeconds(response.refreshTokenExpiry!!.toInt()) it[bridgeUsername] = username } } replier("I have successfully authenticated to Hue!") } catch (ex: Exception) { ex.printStackTrace() replier("+++ Unable to complete action. Please Reinstall Universe And Reboot. +++") } } } } class AccessTokenResponse { @SerializedName("access_token") var accessToken: String? = null @SerializedName("access_token_expires_in") var accessTokenExpiry: String? = null @SerializedName("refresh_token") var refreshToken: String? = null @SerializedName("refresh_token_expires_in") var refreshTokenExpiry: String? = null } class HueLight { var type: String? = null var name: String? = null @SerializedName("manufacturername") var manufacturerName: String? = null @SerializedName("productname") var productName: String? = null } object HueRecords : Table() { val id = varchar("id", 10).primaryKey() val userName = varchar("name", length = 50) val accessToken = varchar("access_token", length = 100) val accessTokenExpiry = datetime("access_token_expiry") val refreshToken = varchar("refresh_token", length = 100) val refreshTokenExpiry = datetime("refresh_token_expiry") val bridgeUsername = varchar("bridge_username", length = 100) }