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")) {