diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 475437d..b743cf8 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -49,8 +49,6 @@ jobs: env: CI_NAME: "GitHub CI" ALPHAVANTAGE_API_KEY: ${{ secrets.ALPHAVANTAGE_API_KEY }} - GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }} - GOOGLE_CSE_CX: ${{ secrets.GOOGLE_CSE_CX }} OWM_API_KEY: ${{ secrets.OWM_API_KEY }} PINBOARD_API_TOKEN: ${{ secrets.PINBOARD_API_TOKEN }} TWITTER_CONSUMERKEY: ${{ secrets.TWITTER_CONSUMERKEY }} @@ -58,8 +56,6 @@ jobs: TWITTER_HANDLE: ${{ secrets.TWITTER_HANDLE }} TWITTER_TOKEN: ${{ secrets.TWITTER_TOKEN }} TWITTER_TOKENSECRET: ${{ secrets.TWITTER_TOKENSECRET }} - WOLFRAM_API_KEY: ${{ secrets.WOLFRAM_API_KEY }} - WOLFRAM_UNITS: ${{ secrets.WOLFRAM_UNITS }} run: ./gradlew build check --stacktrace - name: SonarCloud diff --git a/config/detekt/baseline.xml b/config/detekt/baseline.xml index e5456b7..8cc68a2 100644 --- a/config/detekt/baseline.xml +++ b/config/detekt/baseline.xml @@ -31,6 +31,8 @@ MagicNumber:Twitter.kt$Twitter$60L MagicNumber:TwitterOAuth.kt$TwitterOAuth$401 MagicNumber:Users.kt$Users$8 + MagicNumber:Utils.kt$Utils$200 + MagicNumber:Utils.kt$Utils$399 MagicNumber:Weather2.kt$Weather2.Companion$1.60934 MagicNumber:Weather2.kt$Weather2.Companion$32 MagicNumber:Weather2.kt$Weather2.Companion$404 @@ -65,12 +67,12 @@ 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?, quotaUser: String = ReleaseInfo.PROJECT ): 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 ThrowsCount:Weather2.kt$Weather2.Companion$@JvmStatic @Throws(ModuleException::class) fun getWeather(query: String, apiKey: String?): List<Message> + ThrowsCount:WolframAlpha.kt$WolframAlpha.Companion$@JvmStatic @Throws(ModuleException::class) fun queryWolfram(query: String, units: String = IMPERIAL, appId: String?): String TooGenericExceptionCaught:StockQuote.kt$StockQuote.Companion$e: NullPointerException TooGenericExceptionCaught:Weather2.kt$Weather2.Companion$e: NullPointerException TooManyFunctions:Mobibot.kt$Mobibot : ListenerAdapter diff --git a/properties/mobibot.properties b/properties/mobibot.properties index 12aa774..facff63 100644 --- a/properties/mobibot.properties +++ b/properties/mobibot.properties @@ -65,7 +65,7 @@ tell-max-size=50 #alphavantage-api-key= # -# Get Wolfram Alpa API key from: https://developer.wolframalpha.com/portal/ +# Get Wolfram Alpa AppID from: https://developer.wolframalpha.com/portal/ # -#wolfram-api-key= +#wolfram-appid= #wolfram-units=imperial diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/Utils.kt b/src/main/kotlin/net/thauvin/erik/mobibot/Utils.kt index 511afb1..fff80ae 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/Utils.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/Utils.kt @@ -41,11 +41,10 @@ import org.pircbotx.hooks.types.GenericMessageEvent import org.slf4j.Logger import java.io.BufferedInputStream import java.io.BufferedOutputStream -import java.io.BufferedReader import java.io.IOException -import java.io.InputStreamReader import java.io.ObjectInputStream import java.io.ObjectOutputStream +import java.net.HttpURLConnection import java.net.URL import java.net.URLEncoder import java.nio.charset.StandardCharsets @@ -56,7 +55,6 @@ import java.time.ZoneId import java.time.format.DateTimeFormatter import java.util.Date import java.util.Properties -import java.util.stream.Collectors import kotlin.io.path.exists import kotlin.io.path.fileSize @@ -186,6 +184,12 @@ object Utils { return event.bot().userChannelDao.getChannel(channel).isOp(event.user) } + /** + * Returns {@code true} if a HTTP status code indicates a successful response. + */ + @JvmStatic + fun Int.isHttpSuccess() = this in 200..399 + /** * Returns the last item of a list of strings or empty if none. */ @@ -402,8 +406,21 @@ object Utils { */ @JvmStatic @Throws(IOException::class) - fun URL.reader(): String { - BufferedReader(InputStreamReader(this.openStream(), StandardCharsets.UTF_8)) - .use { reader -> return reader.lines().collect(Collectors.joining(System.lineSeparator())) } + fun URL.reader(): UrlReaderResponse { + val connection = this.openConnection() as HttpURLConnection + connection.setRequestProperty( + "User-Agent", + "Mozilla/5.0 (Linux x86_64; rv:104.0) Gecko/20100101 Firefox/104.0" + ) + return if (connection.responseCode.isHttpSuccess()) { + UrlReaderResponse(connection.responseCode, connection.inputStream.bufferedReader().readText()) + } else { + UrlReaderResponse(connection.responseCode, connection.errorStream.bufferedReader().readText()) + } } + + /** + * Holds the [URL.reader] response code and body text. + */ + data class UrlReaderResponse(val responseCode: Int, val body: String) } diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/modules/CryptoPrices.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/CryptoPrices.kt index 97161d4..7f9e4b7 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/modules/CryptoPrices.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/CryptoPrices.kt @@ -80,11 +80,11 @@ class CryptoPrices : ThreadedModule() { } catch (e: CryptoException) { if (logger.isWarnEnabled) logger.warn("$debugMessage => ${e.statusCode}", e) e.message?.let { - event.sendMessage(it) + event.respond(it) } } catch (e: IOException) { if (logger.isErrorEnabled) logger.error(debugMessage, e) - event.sendMessage("An IO error has occurred while retrieving the cryptocurrency market price.") + event.respond("An IO error has occurred while retrieving the cryptocurrency market price.") } } else { helpResponse(event) diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/modules/CurrencyConverter.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/CurrencyConverter.kt index 59baa19..712aedf 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/modules/CurrencyConverter.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/CurrencyConverter.kt @@ -143,7 +143,7 @@ class CurrencyConverter : ThreadedModule() { try { val amt = cmds[0].replace(",", "") val url = URL("https://api.exchangerate.host/convert?from=$to&to=$from&amount=$amt") - val json = JSONObject(url.reader()) + val json = JSONObject(url.reader().body) if (json.getBoolean("success")) { PublicMessage( @@ -170,7 +170,7 @@ class CurrencyConverter : ThreadedModule() { fun loadSymbols() { try { val url = URL("https://api.exchangerate.host/symbols") - val json = JSONObject(url.reader()) + val json = JSONObject(url.reader().body) if (json.getBoolean("success")) { val symbols = json.getJSONObject("symbols") for (key in symbols.keys()) { diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/modules/GoogleSearch.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/GoogleSearch.kt index 6550283..53b1b97 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/modules/GoogleSearch.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/GoogleSearch.kt @@ -76,7 +76,7 @@ class GoogleSearch : ThreadedModule() { } catch (e: ModuleException) { if (logger.isWarnEnabled) logger.warn(e.debugMessage, e) e.message?.let { - event.sendMessage(it) + event.respond(it) } } } else { @@ -118,7 +118,7 @@ class GoogleSearch : ThreadedModule() { "https://www.googleapis.com/customsearch/v1?key=$apiKey&cx=$cseKey" + ""aUser=${quotaUser}&q=${query.encodeUrl()}&filter=1&num=5&alt=json" ) - val json = JSONObject(url.reader()) + val json = JSONObject(url.reader().body) if (json.has("items")) { val ja = json.getJSONArray("items") for (i in 0 until ja.length()) { @@ -126,6 +126,10 @@ class GoogleSearch : ThreadedModule() { results.add(NoticeMessage(j.getString("title").unescapeXml())) results.add(NoticeMessage(helpFormat(j.getString("link"), false), Colors.DARK_GREEN)) } + } else if (json.has("error")) { + val error = json.getJSONObject("error") + val message = error.getString("message") + throw ModuleException("searchGoogle($query): ${error.getInt("code")} : $message", message) } else { results.add(ErrorMessage("No results found.", Colors.RED)) } diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/modules/StockQuote.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/StockQuote.kt index ce53561..4c2859f 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/modules/StockQuote.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/StockQuote.kt @@ -70,7 +70,7 @@ class StockQuote : ThreadedModule() { } catch (e: ModuleException) { if (logger.isWarnEnabled) logger.warn(e.debugMessage, e) e.message?.let { - event.sendMessage(it) + event.respond(it) } } } else { @@ -147,7 +147,7 @@ class StockQuote : ThreadedModule() { response = URL( "${ALPHAVANTAGE_URL}SYMBOL_SEARCH&keywords=" + symbol.encodeUrl() + "&apikey=" + apiKey.encodeUrl() - ).reader() + ).reader().body var json = getJsonResponse(response, debugMessage) val symbols = json.getJSONArray("bestMatches") if (symbols.isEmpty) { @@ -160,7 +160,7 @@ class StockQuote : ThreadedModule() { "${ALPHAVANTAGE_URL}GLOBAL_QUOTE&symbol=" + symbolInfo.getString("1. symbol").encodeUrl() + "&apikey=" + apiKey.encodeUrl() - ).reader() + ).reader().body json = getJsonResponse(response, debugMessage) val quote = json.getJSONObject("Global Quote") if (quote.isEmpty) { diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/modules/WolframAlpha.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/WolframAlpha.kt index 69e2f4d..b0dc1bf 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/modules/WolframAlpha.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/WolframAlpha.kt @@ -34,6 +34,7 @@ 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.isHttpSuccess import net.thauvin.erik.mobibot.Utils.reader import net.thauvin.erik.mobibot.Utils.sendMessage import org.pircbotx.hooks.types.GenericMessageEvent @@ -68,13 +69,13 @@ class WolframAlpha : ThreadedModule() { } else { getUnits(properties[WOLFRAM_UNITS_PROP]) }, - apiKey = properties[WOLFRAM_API_KEY_PROP] + appId = properties[WOLFRAM_APPID_KEY] ) ) } catch (e: ModuleException) { if (logger.isWarnEnabled) logger.warn(e.debugMessage, e) e.message?.let { - event.sendMessage(it) + event.respond(it) } } } else { @@ -86,7 +87,7 @@ class WolframAlpha : ThreadedModule() { /** * The Wolfram Alpha API Key property. */ - const val WOLFRAM_API_KEY_PROP = "wolfram-api-key" + const val WOLFRAM_APPID_KEY = "wolfram-appid" /** * The Wolfram units properties @@ -103,14 +104,23 @@ class WolframAlpha : ThreadedModule() { @JvmStatic @Throws(ModuleException::class) - fun queryWolfram(query: String, units: String = IMPERIAL, apiKey: String?): String { - if (!apiKey.isNullOrEmpty()) { + fun queryWolfram(query: String, units: String = IMPERIAL, appId: String?): String { + if (!appId.isNullOrEmpty()) { try { - return URL("${API_URL}${apiKey}&units=${units}&i=" + query.encodeUrl()).reader() + val urlReader = URL("${API_URL}${appId}&units=${units}&i=" + query.encodeUrl()).reader() + if (urlReader.responseCode.isHttpSuccess()) { + return urlReader.body + } else { + throw ModuleException( + "wolfram($query): ${urlReader.responseCode} : ${urlReader.body} ", + urlReader.body.ifEmpty { + "Looks like Wolfram Alpha isn't able to answer that. (${urlReader.responseCode})" + } + ) + } } catch (ioe: IOException) { throw ModuleException( - "wolfram($query): IOE", - "Looks like Wolfram Alpha isn't able to answer that.", + "wolfram($query): IOE", "An IO Error occurred while querying Wolfram Alpha.", ioe ) } } else { @@ -128,6 +138,6 @@ class WolframAlpha : ThreadedModule() { 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) + initProperties(WOLFRAM_APPID_KEY, WOLFRAM_UNITS_PROP) } } diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/PinboardTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/PinboardTest.kt index 076ef1d..56f6fd8 100644 --- a/src/test/kotlin/net/thauvin/erik/mobibot/PinboardTest.kt +++ b/src/test/kotlin/net/thauvin/erik/mobibot/PinboardTest.kt @@ -69,7 +69,7 @@ class PinboardTest : LocalProperties() { private fun validatePin(apiToken: String, url: String, vararg matches: String): Boolean { val response = - URL("https://api.pinboard.in/v1/posts/get?auth_token=${apiToken}&tag=test&" + url.encodeUrl()).reader() + URL("https://api.pinboard.in/v1/posts/get?auth_token=${apiToken}&tag=test&" + url.encodeUrl()).reader().body matches.forEach { if (!response.contains(it)) { diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/UtilsTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/UtilsTest.kt index 5ce2ad3..2e5b7e9 100644 --- a/src/test/kotlin/net/thauvin/erik/mobibot/UtilsTest.kt +++ b/src/test/kotlin/net/thauvin/erik/mobibot/UtilsTest.kt @@ -267,7 +267,7 @@ class UtilsTest { @Test @Throws(IOException::class) fun testUrlReader() { - assertThat(URL("https://postman-echo.com/status/200").reader(), "urlReader()") + assertThat(URL("https://postman-echo.com/status/200").reader().body, "urlReader()") .isEqualTo("{\"status\":200}") } diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/modules/GoogleSearchTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/modules/GoogleSearchTest.kt index 5629f7b..7c15d32 100644 --- a/src/test/kotlin/net/thauvin/erik/mobibot/modules/GoogleSearchTest.kt +++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/GoogleSearchTest.kt @@ -34,6 +34,7 @@ package net.thauvin.erik.mobibot.modules import assertk.all import assertk.assertThat import assertk.assertions.contains +import assertk.assertions.hasMessage import assertk.assertions.hasNoCause import assertk.assertions.isEqualTo import assertk.assertions.isFailure @@ -63,6 +64,11 @@ class GoogleSearchTest : LocalProperties() { assertThat { searchGoogle("test", "apiKey", "") }.isFailure() .isInstanceOf(ModuleException::class.java).hasNoCause() + + assertThat { searchGoogle("test", "apiKey", "cssKey") } + .isFailure() + .isInstanceOf(ModuleException::class.java) + .hasMessage("API key not valid. Please pass a valid API key.") } @Test(groups = ["no-ci", "modules"]) diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/modules/WolframAlphaTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/modules/WolframAlphaTest.kt index 62f3bf6..00b1f42 100644 --- a/src/test/kotlin/net/thauvin/erik/mobibot/modules/WolframAlphaTest.kt +++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/WolframAlphaTest.kt @@ -34,6 +34,7 @@ package net.thauvin.erik.mobibot.modules import assertk.assertThat import assertk.assertions.contains +import assertk.assertions.hasMessage import assertk.assertions.isFailure import assertk.assertions.isInstanceOf import net.thauvin.erik.mobibot.ExceptionSanitizer.sanitize @@ -41,21 +42,29 @@ import net.thauvin.erik.mobibot.LocalProperties import org.testng.annotations.Test class WolframAlphaTest : LocalProperties() { - @Test(groups = ["modules"]) + @Test(groups=["modules"]) + fun testAppId() { + assertThat { WolframAlpha.queryWolfram("1 gallon to liter", appId = "DEMO") } + .isFailure() + .isInstanceOf(ModuleException::class.java) + .hasMessage("Error 1: Invalid appid") + + assertThat { WolframAlpha.queryWolfram("1 gallon to liter", appId = "") } + .isFailure() + .isInstanceOf(ModuleException::class.java) + } + + @Test(groups = ["modules", "no-ci"]) @Throws(ModuleException::class) fun queryWolframTest() { - val apiKey = getProperty(WolframAlpha.WOLFRAM_API_KEY_PROP) + val apiKey = getProperty(WolframAlpha.WOLFRAM_APPID_KEY) try { - assertThat(WolframAlpha.queryWolfram("SFO to SEA", apiKey = apiKey), "SFO to SEA").contains("miles") + assertThat(WolframAlpha.queryWolfram("SFO to SEA", appId = 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")) {