Browse Source

Initial import, with Hue module

master
Chris Smith 5 years ago
commit
3e8b730c18

+ 5
- 0
.gitignore View File

@@ -0,0 +1,5 @@
1
+/out
2
+/.idea
3
+/.gradle
4
+/data.db
5
+*.iml

+ 47
- 0
build.gradle.kts View File

@@ -0,0 +1,47 @@
1
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
2
+
3
+plugins {
4
+    kotlin("jvm") version "1.3.20"
5
+}
6
+
7
+repositories {
8
+    jcenter()
9
+    mavenCentral()
10
+    maven("https://github.com/JetBrains/Exposed")
11
+}
12
+
13
+dependencies {
14
+    implementation(kotlin("stdlib-jdk8", "1.3.20"))
15
+    implementation("com.dmdirc:ktirc:0.7.0")
16
+    implementation("com.ufoscout.properlty:properlty:1.9.0")
17
+    implementation("org.jetbrains.exposed:exposed:0.12.1")
18
+    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.1")
19
+    implementation("io.ktor:ktor-client-core:1.1.2")
20
+    implementation("io.ktor:ktor-client-okhttp:1.1.2")
21
+    implementation("io.ktor:ktor-client-json:1.1.2")
22
+    implementation("io.ktor:ktor-client-gson:1.1.2")
23
+    implementation("org.xerial:sqlite-jdbc:3.21.0.1")
24
+}
25
+
26
+java {
27
+    sourceCompatibility = JavaVersion.VERSION_1_8
28
+    targetCompatibility = JavaVersion.VERSION_1_8
29
+}
30
+
31
+tasks.withType<Wrapper> {
32
+    gradleVersion = "5.1.1"
33
+}
34
+
35
+tasks.withType<KotlinCompile> {
36
+    kotlinOptions {
37
+        jvmTarget = "1.8"
38
+    }
39
+}
40
+
41
+configurations.all {
42
+    resolutionStrategy.eachDependency {
43
+        if (requested.group == "org.jetbrains.kotlin") {
44
+            useVersion("1.3.20")
45
+        }
46
+    }
47
+}

BIN
gradle/wrapper/gradle-wrapper.jar View File


+ 5
- 0
gradle/wrapper/gradle-wrapper.properties View File

@@ -0,0 +1,5 @@
1
+distributionBase=GRADLE_USER_HOME
2
+distributionPath=wrapper/dists
3
+distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-bin.zip
4
+zipStoreBase=GRADLE_USER_HOME
5
+zipStorePath=wrapper/dists

+ 172
- 0
gradlew View File

@@ -0,0 +1,172 @@
1
+#!/usr/bin/env sh
2
+
3
+##############################################################################
4
+##
5
+##  Gradle start up script for UN*X
6
+##
7
+##############################################################################
8
+
9
+# Attempt to set APP_HOME
10
+# Resolve links: $0 may be a link
11
+PRG="$0"
12
+# Need this for relative symlinks.
13
+while [ -h "$PRG" ] ; do
14
+    ls=`ls -ld "$PRG"`
15
+    link=`expr "$ls" : '.*-> \(.*\)$'`
16
+    if expr "$link" : '/.*' > /dev/null; then
17
+        PRG="$link"
18
+    else
19
+        PRG=`dirname "$PRG"`"/$link"
20
+    fi
21
+done
22
+SAVED="`pwd`"
23
+cd "`dirname \"$PRG\"`/" >/dev/null
24
+APP_HOME="`pwd -P`"
25
+cd "$SAVED" >/dev/null
26
+
27
+APP_NAME="Gradle"
28
+APP_BASE_NAME=`basename "$0"`
29
+
30
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31
+DEFAULT_JVM_OPTS=""
32
+
33
+# Use the maximum available, or set MAX_FD != -1 to use that value.
34
+MAX_FD="maximum"
35
+
36
+warn () {
37
+    echo "$*"
38
+}
39
+
40
+die () {
41
+    echo
42
+    echo "$*"
43
+    echo
44
+    exit 1
45
+}
46
+
47
+# OS specific support (must be 'true' or 'false').
48
+cygwin=false
49
+msys=false
50
+darwin=false
51
+nonstop=false
52
+case "`uname`" in
53
+  CYGWIN* )
54
+    cygwin=true
55
+    ;;
56
+  Darwin* )
57
+    darwin=true
58
+    ;;
59
+  MINGW* )
60
+    msys=true
61
+    ;;
62
+  NONSTOP* )
63
+    nonstop=true
64
+    ;;
65
+esac
66
+
67
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68
+
69
+# Determine the Java command to use to start the JVM.
70
+if [ -n "$JAVA_HOME" ] ; then
71
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72
+        # IBM's JDK on AIX uses strange locations for the executables
73
+        JAVACMD="$JAVA_HOME/jre/sh/java"
74
+    else
75
+        JAVACMD="$JAVA_HOME/bin/java"
76
+    fi
77
+    if [ ! -x "$JAVACMD" ] ; then
78
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79
+
80
+Please set the JAVA_HOME variable in your environment to match the
81
+location of your Java installation."
82
+    fi
83
+else
84
+    JAVACMD="java"
85
+    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86
+
87
+Please set the JAVA_HOME variable in your environment to match the
88
+location of your Java installation."
89
+fi
90
+
91
+# Increase the maximum file descriptors if we can.
92
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93
+    MAX_FD_LIMIT=`ulimit -H -n`
94
+    if [ $? -eq 0 ] ; then
95
+        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96
+            MAX_FD="$MAX_FD_LIMIT"
97
+        fi
98
+        ulimit -n $MAX_FD
99
+        if [ $? -ne 0 ] ; then
100
+            warn "Could not set maximum file descriptor limit: $MAX_FD"
101
+        fi
102
+    else
103
+        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104
+    fi
105
+fi
106
+
107
+# For Darwin, add options to specify how the application appears in the dock
108
+if $darwin; then
109
+    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110
+fi
111
+
112
+# For Cygwin, switch paths to Windows format before running java
113
+if $cygwin ; then
114
+    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115
+    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116
+    JAVACMD=`cygpath --unix "$JAVACMD"`
117
+
118
+    # We build the pattern for arguments to be converted via cygpath
119
+    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120
+    SEP=""
121
+    for dir in $ROOTDIRSRAW ; do
122
+        ROOTDIRS="$ROOTDIRS$SEP$dir"
123
+        SEP="|"
124
+    done
125
+    OURCYGPATTERN="(^($ROOTDIRS))"
126
+    # Add a user-defined pattern to the cygpath arguments
127
+    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128
+        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129
+    fi
130
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
131
+    i=0
132
+    for arg in "$@" ; do
133
+        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134
+        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
135
+
136
+        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
137
+            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138
+        else
139
+            eval `echo args$i`="\"$arg\""
140
+        fi
141
+        i=$((i+1))
142
+    done
143
+    case $i in
144
+        (0) set -- ;;
145
+        (1) set -- "$args0" ;;
146
+        (2) set -- "$args0" "$args1" ;;
147
+        (3) set -- "$args0" "$args1" "$args2" ;;
148
+        (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149
+        (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150
+        (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151
+        (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152
+        (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153
+        (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154
+    esac
155
+fi
156
+
157
+# Escape application args
158
+save () {
159
+    for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160
+    echo " "
161
+}
162
+APP_ARGS=$(save "$@")
163
+
164
+# Collect all arguments for the java command, following the shell quoting and substitution rules
165
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166
+
167
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169
+  cd "$(dirname "$0")"
170
+fi
171
+
172
+exec "$JAVACMD" "$@"

+ 84
- 0
gradlew.bat View File

@@ -0,0 +1,84 @@
1
+@if "%DEBUG%" == "" @echo off
2
+@rem ##########################################################################
3
+@rem
4
+@rem  Gradle startup script for Windows
5
+@rem
6
+@rem ##########################################################################
7
+
8
+@rem Set local scope for the variables with windows NT shell
9
+if "%OS%"=="Windows_NT" setlocal
10
+
11
+set DIRNAME=%~dp0
12
+if "%DIRNAME%" == "" set DIRNAME=.
13
+set APP_BASE_NAME=%~n0
14
+set APP_HOME=%DIRNAME%
15
+
16
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17
+set DEFAULT_JVM_OPTS=
18
+
19
+@rem Find java.exe
20
+if defined JAVA_HOME goto findJavaFromJavaHome
21
+
22
+set JAVA_EXE=java.exe
23
+%JAVA_EXE% -version >NUL 2>&1
24
+if "%ERRORLEVEL%" == "0" goto init
25
+
26
+echo.
27
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28
+echo.
29
+echo Please set the JAVA_HOME variable in your environment to match the
30
+echo location of your Java installation.
31
+
32
+goto fail
33
+
34
+:findJavaFromJavaHome
35
+set JAVA_HOME=%JAVA_HOME:"=%
36
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37
+
38
+if exist "%JAVA_EXE%" goto init
39
+
40
+echo.
41
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42
+echo.
43
+echo Please set the JAVA_HOME variable in your environment to match the
44
+echo location of your Java installation.
45
+
46
+goto fail
47
+
48
+:init
49
+@rem Get command-line arguments, handling Windows variants
50
+
51
+if not "%OS%" == "Windows_NT" goto win9xME_args
52
+
53
+:win9xME_args
54
+@rem Slurp the command line arguments.
55
+set CMD_LINE_ARGS=
56
+set _SKIP=2
57
+
58
+:win9xME_args_slurp
59
+if "x%~1" == "x" goto execute
60
+
61
+set CMD_LINE_ARGS=%*
62
+
63
+:execute
64
+@rem Setup the command line
65
+
66
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67
+
68
+@rem Execute Gradle
69
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70
+
71
+:end
72
+@rem End local scope for the variables with windows NT shell
73
+if "%ERRORLEVEL%"=="0" goto mainEnd
74
+
75
+:fail
76
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77
+rem the _cmd.exe /c_ return code!
78
+if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79
+exit /b 1
80
+
81
+:mainEnd
82
+if "%OS%"=="Windows_NT" endlocal
83
+
84
+:omega

+ 17
- 0
src/README.md View File

@@ -0,0 +1,17 @@
1
+## Configuration
2
+
3
+* `irc`
4
+  * `server` - hostname or IP to connect to [required]
5
+  * `port` - port to connect on [required]
6
+  * `tls` - whether to use TLS or not [default false]
7
+  * `password` - server password [default none]
8
+  * `nickname` - nickname to use [default Hex]
9
+  * `realname` - real name to use [default +++Divide By Cucumber Error+++]
10
+  * `ident` - ident/username to use [default hexbot]
11
+  * `nickserv` [optional]
12
+    * `user` - username to identify to nickserv with
13
+    * `password` - password to identify to nickserv with
14
+* `hue`
15
+  * `appid` - the application ID to use for the Hue remote API
16
+  * `clientid` - the client ID to use for the Hue remote API
17
+  * `clientsecret` - the secret to use for the Hue remote API

+ 95
- 0
src/main/kotlin/com/chameth/hexbot/Hexbot.kt View File

@@ -0,0 +1,95 @@
1
+package com.chameth.hexbot
2
+
3
+import com.chameth.hexbot.modules.Hue
4
+import com.chameth.hexbot.modules.Module
5
+import com.dmdirc.ktirc.IrcClient
6
+import com.dmdirc.ktirc.events.IrcEvent
7
+import com.dmdirc.ktirc.events.ServerDisconnected
8
+import com.dmdirc.ktirc.events.ServerReady
9
+import com.dmdirc.ktirc.messages.sendJoin
10
+import com.ufoscout.properlty.Properlty
11
+import com.ufoscout.properlty.reader.EnvironmentVariablesReader
12
+import com.ufoscout.properlty.reader.PropertiesResourceReader
13
+import kotlinx.coroutines.Dispatchers
14
+import kotlinx.coroutines.GlobalScope
15
+import kotlinx.coroutines.launch
16
+import kotlinx.coroutines.runBlocking
17
+import org.jetbrains.exposed.sql.Database
18
+import java.util.logging.Level
19
+import java.util.logging.LogManager
20
+
21
+class HexBot(private val config: Properlty, availableModules: List<Module>) {
22
+
23
+    private var client: IrcClient? = null
24
+
25
+    private val channels
26
+        get() = config.getArray("irc.channels")
27
+
28
+    private val modules = availableModules.filter { it.init(config) }
29
+
30
+    fun connect() {
31
+        with (IrcClient {
32
+            server {
33
+                host = config["irc.server"].orElseThrow { IllegalStateException("irc.server not configured") }
34
+                port = config.getInt("irc.port").orElseThrow { IllegalStateException("irc.port not configured") }
35
+                useTls = config.getBoolean("irc.tls", false)
36
+                password = config["irc.password", null]
37
+            }
38
+
39
+            profile {
40
+                nickname = config["irc.nickname", "Hex"]
41
+                realName = config["irc.realname", "+++Divide By Cucumber Error+++"]
42
+                username = config["irc.ident", "hexbot"]
43
+            }
44
+
45
+            sasl {
46
+                username = config["irc.nickserv.user", null]
47
+                password = config["irc.nickserv.password", null]
48
+            }
49
+        }) {
50
+            client = this
51
+            onEvent {
52
+                handleEvent(this, it)
53
+            }
54
+            connect()
55
+        }
56
+    }
57
+
58
+    private fun handleEvent(client: IrcClient, event: IrcEvent) {
59
+        when (event) {
60
+            is ServerReady -> joinChannels()
61
+            is ServerDisconnected -> client.connect()
62
+        }
63
+        for (module in modules) {
64
+            GlobalScope.launch(Dispatchers.IO) {
65
+                module.processEvent(client, event)
66
+            }
67
+        }
68
+    }
69
+
70
+    private fun joinChannels() {
71
+        channels.forEach { client?.sendJoin(it) }
72
+    }
73
+
74
+}
75
+
76
+internal fun loadConfig(): Properlty = Properlty
77
+        .builder()
78
+        .caseSensitive(false)
79
+        .add(PropertiesResourceReader.build("./hexbot.properties").ignoreNotFound(true))
80
+        .add(EnvironmentVariablesReader().replace("HEXBOT_", "").replace("_", "."))
81
+        .build()
82
+
83
+fun main() = runBlocking<Unit> {
84
+    val rootLogger = LogManager.getLogManager().getLogger("")
85
+    rootLogger.level = Level.FINEST
86
+    for (h in rootLogger.handlers) {
87
+        h.level = Level.FINEST
88
+    }
89
+
90
+    Database.connect("jdbc:sqlite:data.db", "org.sqlite.JDBC")
91
+
92
+    val bot = HexBot(loadConfig(), listOf(Hue()))
93
+    bot.connect()
94
+    readLine()
95
+}

+ 31
- 0
src/main/kotlin/com/chameth/hexbot/Utils.kt View File

@@ -0,0 +1,31 @@
1
+package com.chameth.hexbot
2
+
3
+import com.dmdirc.ktirc.IrcClient
4
+import com.dmdirc.ktirc.events.IrcEvent
5
+import com.dmdirc.ktirc.events.MessageReceived
6
+import kotlin.contracts.ExperimentalContracts
7
+import kotlin.contracts.contract
8
+
9
+@ExperimentalContracts
10
+fun IrcClient.isModuleCommand(ircEvent: IrcEvent, module: String): Boolean {
11
+    contract {
12
+        returns(true) implies (ircEvent is MessageReceived)
13
+    }
14
+    return isBotCommand(ircEvent) && with(ircEvent.message.split(' ')) { this.size > 1 && this[1].toLowerCase() == module.toLowerCase() }
15
+}
16
+
17
+@ExperimentalContracts
18
+fun IrcClient.isBotCommand(ircEvent: IrcEvent): Boolean {
19
+    contract {
20
+        returns(true) implies (ircEvent is MessageReceived)
21
+    }
22
+    return ircEvent is MessageReceived && ircEvent.message.isBotCommand(serverState.localNickname)
23
+}
24
+
25
+val MessageReceived.moduleCommandArgs
26
+    get() = message.split(' ').drop(2)
27
+
28
+private fun String.isBotCommand(nickname: String) =
29
+        startsWith("$nickname ", ignoreCase=true) ||
30
+        startsWith("$nickname: ", ignoreCase=true) ||
31
+        startsWith("$nickname, ", ignoreCase=true)

+ 210
- 0
src/main/kotlin/com/chameth/hexbot/modules/Hue.kt View File

@@ -0,0 +1,210 @@
1
+package com.chameth.hexbot.modules
2
+
3
+import com.chameth.hexbot.isModuleCommand
4
+import com.chameth.hexbot.moduleCommandArgs
5
+import com.dmdirc.ktirc.IrcClient
6
+import com.dmdirc.ktirc.events.IrcEvent
7
+import com.dmdirc.ktirc.events.MessageReceived
8
+import com.dmdirc.ktirc.events.react
9
+import com.dmdirc.ktirc.events.reply
10
+import com.google.gson.annotations.SerializedName
11
+import com.ufoscout.properlty.Properlty
12
+import io.ktor.client.HttpClient
13
+import io.ktor.client.engine.okhttp.OkHttp
14
+import io.ktor.client.features.json.GsonSerializer
15
+import io.ktor.client.features.json.JsonFeature
16
+import io.ktor.client.request.*
17
+import org.jetbrains.exposed.sql.SchemaUtils
18
+import org.jetbrains.exposed.sql.Table
19
+import org.jetbrains.exposed.sql.insert
20
+import org.jetbrains.exposed.sql.select
21
+import org.jetbrains.exposed.sql.transactions.transaction
22
+import org.joda.time.DateTime
23
+import java.sql.Connection
24
+import java.util.*
25
+import kotlin.contracts.ExperimentalContracts
26
+
27
+class Hue : Module {
28
+
29
+    private var appId: String? = null
30
+    private var clientId: String? = null
31
+    private var clientSecret: String? = null
32
+    private val basicHeader by lazy { Base64.getEncoder().encodeToString("$clientId:$clientSecret".toByteArray()) }
33
+
34
+    private val httpClient = HttpClient(OkHttp) {
35
+        engine {
36
+            config {
37
+                followRedirects(true)
38
+            }
39
+        }
40
+        install(JsonFeature) {
41
+            serializer = GsonSerializer()
42
+        }
43
+    }
44
+
45
+    override fun init(settings: Properlty): Boolean {
46
+        clientId = settings["hue.clientid", null]
47
+        clientSecret = settings["hue.clientsecret", null]
48
+        appId = settings["hue.appid", null]
49
+
50
+        if (clientId == null || clientSecret == null || appId == null) {
51
+            return false
52
+        }
53
+
54
+        transaction(Connection.TRANSACTION_SERIALIZABLE, 1) {
55
+            SchemaUtils.create(HueRecords)
56
+        }
57
+
58
+        return true
59
+    }
60
+
61
+    @ExperimentalContracts
62
+    override suspend fun processEvent(client: IrcClient, event: IrcEvent) {
63
+        if (client.isModuleCommand(event, "hue")) {
64
+            val account = event.user.account ?: client.userState[event.user]?.details?.account
65
+            if (account == null) {
66
+                client.reply(event, "You must be authenticated to use this command", true)
67
+                return
68
+            }
69
+
70
+            val args = event.moduleCommandArgs
71
+            when (args.firstOrNull()) {
72
+                "auth" -> handleAuth(args.getOrNull(1), account) { client.reply(event, it, true) }
73
+                "list" -> handleList(account) { client.reply(event, it, true) }
74
+                "turn" -> handleTurn(account, args[1].toInt(), args[2] == "on", client, event)
75
+            }
76
+        }
77
+    }
78
+
79
+    private suspend fun handleTurn(account: String, id: Int, state: Boolean, ircClient: IrcClient, event: MessageReceived) {
80
+        lateinit var token: String
81
+        lateinit var username: String
82
+
83
+        // TODO: This continues with HTTP request even if the record is empty
84
+        transaction(Connection.TRANSACTION_SERIALIZABLE, 1) {
85
+            val records = HueRecords.select { HueRecords.userName eq account }
86
+            if (records.empty()) {
87
+                ircClient.reply(event, "You do not seem to have set up Hue. Use the 'hue auth' command to get started.")
88
+                return@transaction
89
+            }
90
+
91
+            val record = records.first()
92
+            token = record[HueRecords.accessToken]
93
+            username = record[HueRecords.bridgeUsername]
94
+        }
95
+
96
+        try {
97
+            httpClient.put<Unit>("https://api.meethue.com/bridge/$username/lights/$id/state") {
98
+                header("Authorization", "Bearer $token")
99
+                header("Content-type", "application/json")
100
+                body = mapOf("on" to state)
101
+            }
102
+            ircClient.react(event, "💡 ${if (state) "On" else "Off"}")
103
+        } catch (ex : Exception) {
104
+            ircClient.reply(event, "+++ Unable to complete action. Please Reinstall Universe And Reboot. +++")
105
+        }
106
+    }
107
+
108
+    private suspend fun handleList(account: String, replier: (String) -> Unit) {
109
+        lateinit var token: String
110
+        lateinit var username: String
111
+
112
+        transaction(Connection.TRANSACTION_SERIALIZABLE, 1) {
113
+            val records = HueRecords.select { HueRecords.userName eq account }
114
+            if (records.empty()) {
115
+                replier("You do not seem to have set up Hue. Use the 'hue auth' command to get started.")
116
+                return@transaction
117
+            }
118
+
119
+            val record = records.first()
120
+            token = record[HueRecords.accessToken]
121
+            username = record[HueRecords.bridgeUsername]
122
+        }
123
+
124
+        val lights = httpClient.get<Map<String, HueLight>>("https://api.meethue.com/bridge/$username/lights") {
125
+            header("Authorization", "Bearer $token")
126
+        }
127
+        lights.forEach { id, light ->
128
+            replier("Light $id is a ${light.manufacturerName} ${light.productName} named ${light.name}")
129
+        }
130
+    }
131
+
132
+    private suspend fun handleAuth(arg: String?, account: String, replier: (String) -> Unit) {
133
+        when (arg) {
134
+            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")
135
+            else ->
136
+                try {
137
+                    val response = httpClient.post<AccessTokenResponse>("https://api.meethue.com/oauth2/token?code=$arg&grant_type=authorization_code") {
138
+                        body = ""
139
+                        headers {
140
+                            set("Authorization", "Basic $basicHeader")
141
+                        }
142
+                    }
143
+
144
+                    httpClient.put<Unit>("https://api.meethue.com/bridge/0/config") {
145
+                        headers {
146
+                            set("Authorization", "Bearer ${response.accessToken}")
147
+                            set("Content-Type", "application/json")
148
+                        }
149
+                        body = mapOf("linkbutton" to true)
150
+                    }
151
+
152
+                    val username = httpClient.post<List<Map<String, Map<String, String>>>>("https://api.meethue.com/bridge/") {
153
+                        headers {
154
+                            set("Authorization", "Bearer ${response.accessToken}")
155
+                            set("Content-Type", "application/json")
156
+                        }
157
+                        body = mapOf("devicetype" to "hexbot")
158
+                    }[0]["success"]?.getValue("username")!!
159
+
160
+                    transaction(Connection.TRANSACTION_SERIALIZABLE, 1) {
161
+                        HueRecords.insert {
162
+                            it[userName] = account
163
+                            it[accessToken] = response.accessToken!!
164
+                            it[accessTokenExpiry] = DateTime.now().plusSeconds(response.accessTokenExpiry!!.toInt())
165
+                            it[refreshToken] = response.refreshToken!!
166
+                            it[refreshTokenExpiry] = DateTime.now().plusSeconds(response.refreshTokenExpiry!!.toInt())
167
+                            it[bridgeUsername] = username
168
+                        }
169
+                    }
170
+
171
+                    replier("I have successfully authenticated to Hue!")
172
+                } catch (ex: Exception) {
173
+                    ex.printStackTrace()
174
+                    replier("+++ Unable to complete action. Please Reinstall Universe And Reboot. +++")
175
+            }
176
+        }
177
+    }
178
+
179
+}
180
+
181
+class AccessTokenResponse {
182
+    @SerializedName("access_token")
183
+    var accessToken: String? = null
184
+
185
+    @SerializedName("access_token_expires_in")
186
+    var accessTokenExpiry: String? = null
187
+
188
+    @SerializedName("refresh_token")
189
+    var refreshToken: String? = null
190
+
191
+    @SerializedName("refresh_token_expires_in")
192
+    var refreshTokenExpiry: String? = null
193
+}
194
+
195
+class HueLight {
196
+    var type: String? = null
197
+    var name: String? = null
198
+    @SerializedName("manufacturername") var manufacturerName: String? = null
199
+    @SerializedName("productname") var productName: String? = null
200
+}
201
+
202
+object HueRecords : Table() {
203
+    val id = varchar("id", 10).primaryKey()
204
+    val userName = varchar("name", length = 50)
205
+    val accessToken = varchar("access_token", length = 100)
206
+    val accessTokenExpiry = datetime("access_token_expiry")
207
+    val refreshToken = varchar("refresh_token", length = 100)
208
+    val refreshTokenExpiry = datetime("refresh_token_expiry")
209
+    val bridgeUsername = varchar("bridge_username", length = 100)
210
+}

+ 13
- 0
src/main/kotlin/com/chameth/hexbot/modules/Module.kt View File

@@ -0,0 +1,13 @@
1
+package com.chameth.hexbot.modules
2
+
3
+import com.dmdirc.ktirc.IrcClient
4
+import com.dmdirc.ktirc.events.IrcEvent
5
+import com.ufoscout.properlty.Properlty
6
+
7
+interface Module {
8
+
9
+    fun init(settings: Properlty): Boolean
10
+
11
+    suspend fun processEvent(client: IrcClient, event: IrcEvent)
12
+
13
+}

Loading…
Cancel
Save