From 3ba2dfba4afa4a95e43f8f28e9a99a68d3e4f737 Mon Sep 17 00:00:00 2001 From: "Erik C. Thauvin" Date: Thu, 15 Sep 2022 23:28:08 -0700 Subject: [PATCH] Implemented module to query Wolfram Alpha --- config/detekt/baseline.xml | 2 + properties/mobibot.properties | 6 + .../net/thauvin/erik/mobibot/Mobibot.kt | 2 + .../erik/mobibot/modules/WolframAlpha.kt | 133 ++++++++++++++++++ .../erik/mobibot/modules/WolframAlphaTest.kt | 68 +++++++++ website/index.html | 4 + 6 files changed, 215 insertions(+) create mode 100644 src/main/kotlin/net/thauvin/erik/mobibot/modules/WolframAlpha.kt create mode 100644 src/test/kotlin/net/thauvin/erik/mobibot/modules/WolframAlphaTest.kt diff --git a/config/detekt/baseline.xml b/config/detekt/baseline.xml index c9c57d9..81962fb 100644 --- a/config/detekt/baseline.xml +++ b/config/detekt/baseline.xml @@ -66,6 +66,8 @@ ReturnCount:ExceptionSanitizer.kt$ExceptionSanitizer$fun ModuleException.sanitize(vararg sanitize: String): ModuleException SwallowedException:GoogleSearchTest.kt$GoogleSearchTest$e: ModuleException SwallowedException:StockQuoteTest.kt$StockQuoteTest$e: ModuleException + SwallowedException:WolframAlpha.kt$WolframAlpha.Companion$ioe: IOException + SwallowedException:WolframAlphaTest.kt$WolframAlphaTest$e: ModuleException ThrowsCount:GoogleSearch.kt$GoogleSearch.Companion$@JvmStatic @Throws(ModuleException::class) fun searchGoogle(query: String, apiKey: String?, cseKey: String?): List<Message> ThrowsCount:StockQuote.kt$StockQuote.Companion$@JvmStatic @Throws(ModuleException::class) fun getQuote(symbol: String, apiKey: String?): List<Message> ThrowsCount:StockQuote.kt$StockQuote.Companion$@Throws(ModuleException::class) private fun getJsonResponse(response: String, debugMessage: String): JSONObject diff --git a/properties/mobibot.properties b/properties/mobibot.properties index 28f5a8d..12aa774 100644 --- a/properties/mobibot.properties +++ b/properties/mobibot.properties @@ -63,3 +63,9 @@ tell-max-size=50 # Get Alpha Vantage Stock Quote API key from: https://www.alphavantage.co/support/#api-key # #alphavantage-api-key= + +# +# Get Wolfram Alpa API key from: https://developer.wolframalpha.com/portal/ +# +#wolfram-api-key= +#wolfram-units=imperial diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/Mobibot.kt b/src/main/kotlin/net/thauvin/erik/mobibot/Mobibot.kt index 6cbefa9..7186f7e 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/Mobibot.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/Mobibot.kt @@ -79,6 +79,7 @@ import net.thauvin.erik.mobibot.modules.RockPaperScissors import net.thauvin.erik.mobibot.modules.StockQuote import net.thauvin.erik.mobibot.modules.War import net.thauvin.erik.mobibot.modules.Weather2 +import net.thauvin.erik.mobibot.modules.WolframAlpha import net.thauvin.erik.mobibot.modules.WorldTime import net.thauvin.erik.semver.Version import org.pircbotx.Configuration @@ -426,6 +427,7 @@ class Mobibot(nickname: String, val channel: String, logsDirPath: String, p: Pro addons.add(Weather2()) addons.add(WorldTime()) addons.add(War()) + addons.add(WolframAlpha()) // Sort the addons addons.names.sort() diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/modules/WolframAlpha.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/WolframAlpha.kt new file mode 100644 index 0000000..69e2f4d --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/WolframAlpha.kt @@ -0,0 +1,133 @@ +/* + * WolframAlpha.kt + * + * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net) + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package net.thauvin.erik.mobibot.modules + +import net.thauvin.erik.mobibot.Utils +import net.thauvin.erik.mobibot.Utils.encodeUrl +import net.thauvin.erik.mobibot.Utils.reader +import net.thauvin.erik.mobibot.Utils.sendMessage +import org.pircbotx.hooks.types.GenericMessageEvent +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.io.IOException +import java.net.URL + +class WolframAlpha : ThreadedModule() { + private val logger: Logger = LoggerFactory.getLogger(WolframAlpha::class.java) + + override val name = "WolframAlpha" + + private fun getUnits(unit: String?): String { + println("--> $unit") + return if (unit?.lowercase() == METRIC) { + METRIC + } else { + IMPERIAL + } + } + + override fun run(channel: String, cmd: String, args: String, event: GenericMessageEvent) { + if (args.isNotBlank()) { + try { + val query = args.trim().split("units=", limit = 2, ignoreCase = true) + event.sendMessage( + queryWolfram( + query[0].trim(), + units = if (query.size == 2) { + getUnits(query[1].trim()) + } else { + getUnits(properties[WOLFRAM_UNITS_PROP]) + }, + apiKey = properties[WOLFRAM_API_KEY_PROP] + ) + ) + } catch (e: ModuleException) { + if (logger.isWarnEnabled) logger.warn(e.debugMessage, e) + e.message?.let { + event.sendMessage(it) + } + } + } else { + helpResponse(event) + } + } + + companion object { + /** + * The Wolfram Alpha API Key property. + */ + const val WOLFRAM_API_KEY_PROP = "wolfram-api-key" + + /** + * The Wolfram units properties + */ + const val WOLFRAM_UNITS_PROP = "wolfram-units" + const val METRIC = "metric" + const val IMPERIAL = "imperial" + + // Wolfram command + private const val WOLFRAM_CMD = "wolfram" + + // Wolfram Alpha API URL + private const val API_URL = "http://api.wolframalpha.com/v1/spoken?appid=" + + @JvmStatic + @Throws(ModuleException::class) + fun queryWolfram(query: String, units: String = IMPERIAL, apiKey: String?): String { + if (!apiKey.isNullOrEmpty()) { + try { + return URL("${API_URL}${apiKey}&units=${units}&i=" + query.encodeUrl()).reader() + } catch (ioe: IOException) { + throw ModuleException( + "wolfram($query): IOE", + "Looks like Wolfram Alpha isn't able to answer that.", + ) + } + } else { + throw ModuleException("wolfram($query): No API Key", "No Wolfram Alpha API key specified.") + } + } + } + + init { + commands.add(WOLFRAM_CMD) + with(help) { + add("To get answers from Wolfram Alpha:") + add(Utils.helpFormat("%c $WOLFRAM_CMD [units=(${METRIC}|${IMPERIAL})]")) + add("For example:") + add(Utils.helpFormat("%c $WOLFRAM_CMD days until christmas")) + add(Utils.helpFormat("%c $WOLFRAM_CMD distance earth moon units=metric")) + } + initProperties(WOLFRAM_API_KEY_PROP, WOLFRAM_UNITS_PROP) + } +} diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/modules/WolframAlphaTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/modules/WolframAlphaTest.kt new file mode 100644 index 0000000..3150f2f --- /dev/null +++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/WolframAlphaTest.kt @@ -0,0 +1,68 @@ +/* + * WolframAlphaTest.kt + * + * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net) + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package net.thauvin.erik.mobibot.modules + +import assertk.assertThat +import assertk.assertions.contains +import assertk.assertions.isFailure +import assertk.assertions.isInstanceOf +import net.thauvin.erik.mobibot.ExceptionSanitizer.sanitize +import net.thauvin.erik.mobibot.LocalProperties +import org.testng.annotations.Test + +class WolframAlphaTest : LocalProperties() { + @Test + @Throws(ModuleException::class) + fun queryWolframTest() { + val apiKey = getProperty(WolframAlpha.WOLFRAM_API_KEY_PROP) + try { + assertThat(WolframAlpha.queryWolfram("SFO to SEA", apiKey = apiKey), "SFO to SEA").contains("miles") + + assertThat( + WolframAlpha.queryWolfram("SFO to LAX", WolframAlpha.METRIC, apiKey), + "SFO to LA" + ).contains("kilometers") + + assertThat { WolframAlpha.queryWolfram("1 gallon to liter", apiKey = "") } + .isFailure() + .isInstanceOf(ModuleException::class.java) + } catch (e: ModuleException) { + // Avoid displaying api key in CI logs + if ("true" == System.getenv("CI")) { + throw e.sanitize(apiKey) + } else { + throw e + } + } + } +} diff --git a/website/index.html b/website/index.html index acab3b3..6904a5f 100644 --- a/website/index.html +++ b/website/index.html @@ -80,6 +80,10 @@
  • Performing Google searches
    mobibot: google mobitopia on irc
  • +
  • Getting answers from Wolfram Alpha +
    mobibot: wolfram days until christmas
    +
    mobibot: wolfram 1 gallon to liter
    +
  • Displaying weather information
    mobibot: weather san francisco
    mobibot: weather 94123