Reworked the URL reader to include the code and body text when an error is returned.

This commit is contained in:
Erik C. Thauvin 2022-09-18 23:28:13 -07:00
parent 2a2ab39b5f
commit 951fdaa5f7
13 changed files with 84 additions and 40 deletions

View file

@ -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

View file

@ -31,6 +31,8 @@
<ID>MagicNumber:Twitter.kt$Twitter$60L</ID>
<ID>MagicNumber:TwitterOAuth.kt$TwitterOAuth$401</ID>
<ID>MagicNumber:Users.kt$Users$8</ID>
<ID>MagicNumber:Utils.kt$Utils$200</ID>
<ID>MagicNumber:Utils.kt$Utils$399</ID>
<ID>MagicNumber:Weather2.kt$Weather2.Companion$1.60934</ID>
<ID>MagicNumber:Weather2.kt$Weather2.Companion$32</ID>
<ID>MagicNumber:Weather2.kt$Weather2.Companion$404</ID>
@ -65,12 +67,12 @@
<ID>ReturnCount:ExceptionSanitizer.kt$ExceptionSanitizer$fun ModuleException.sanitize(vararg sanitize: String): ModuleException</ID>
<ID>SwallowedException:GoogleSearchTest.kt$GoogleSearchTest$e: ModuleException</ID>
<ID>SwallowedException:StockQuoteTest.kt$StockQuoteTest$e: ModuleException</ID>
<ID>SwallowedException:WolframAlpha.kt$WolframAlpha.Companion$ioe: IOException</ID>
<ID>SwallowedException:WolframAlphaTest.kt$WolframAlphaTest$e: ModuleException</ID>
<ID>ThrowsCount:GoogleSearch.kt$GoogleSearch.Companion$@JvmStatic @Throws(ModuleException::class) fun searchGoogle( query: String, apiKey: String?, cseKey: String?, quotaUser: String = ReleaseInfo.PROJECT ): List&lt;Message&gt;</ID>
<ID>ThrowsCount:StockQuote.kt$StockQuote.Companion$@JvmStatic @Throws(ModuleException::class) fun getQuote(symbol: String, apiKey: String?): List&lt;Message&gt;</ID>
<ID>ThrowsCount:StockQuote.kt$StockQuote.Companion$@Throws(ModuleException::class) private fun getJsonResponse(response: String, debugMessage: String): JSONObject</ID>
<ID>ThrowsCount:Weather2.kt$Weather2.Companion$@JvmStatic @Throws(ModuleException::class) fun getWeather(query: String, apiKey: String?): List&lt;Message&gt;</ID>
<ID>ThrowsCount:WolframAlpha.kt$WolframAlpha.Companion$@JvmStatic @Throws(ModuleException::class) fun queryWolfram(query: String, units: String = IMPERIAL, appId: String?): String</ID>
<ID>TooGenericExceptionCaught:StockQuote.kt$StockQuote.Companion$e: NullPointerException</ID>
<ID>TooGenericExceptionCaught:Weather2.kt$Weather2.Companion$e: NullPointerException</ID>
<ID>TooManyFunctions:Mobibot.kt$Mobibot : ListenerAdapter</ID>

View file

@ -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

View file

@ -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)
}

View file

@ -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)

View file

@ -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()) {

View file

@ -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" +
"&quotaUser=${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))
}

View file

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

View file

@ -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)
}
}

View file

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

View file

@ -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}")
}

View file

@ -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"])

View file

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