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: env:
CI_NAME: "GitHub CI" CI_NAME: "GitHub CI"
ALPHAVANTAGE_API_KEY: ${{ secrets.ALPHAVANTAGE_API_KEY }} 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 }} OWM_API_KEY: ${{ secrets.OWM_API_KEY }}
PINBOARD_API_TOKEN: ${{ secrets.PINBOARD_API_TOKEN }} PINBOARD_API_TOKEN: ${{ secrets.PINBOARD_API_TOKEN }}
TWITTER_CONSUMERKEY: ${{ secrets.TWITTER_CONSUMERKEY }} TWITTER_CONSUMERKEY: ${{ secrets.TWITTER_CONSUMERKEY }}
@ -58,8 +56,6 @@ jobs:
TWITTER_HANDLE: ${{ secrets.TWITTER_HANDLE }} TWITTER_HANDLE: ${{ secrets.TWITTER_HANDLE }}
TWITTER_TOKEN: ${{ secrets.TWITTER_TOKEN }} TWITTER_TOKEN: ${{ secrets.TWITTER_TOKEN }}
TWITTER_TOKENSECRET: ${{ secrets.TWITTER_TOKENSECRET }} TWITTER_TOKENSECRET: ${{ secrets.TWITTER_TOKENSECRET }}
WOLFRAM_API_KEY: ${{ secrets.WOLFRAM_API_KEY }}
WOLFRAM_UNITS: ${{ secrets.WOLFRAM_UNITS }}
run: ./gradlew build check --stacktrace run: ./gradlew build check --stacktrace
- name: SonarCloud - name: SonarCloud

View file

@ -31,6 +31,8 @@
<ID>MagicNumber:Twitter.kt$Twitter$60L</ID> <ID>MagicNumber:Twitter.kt$Twitter$60L</ID>
<ID>MagicNumber:TwitterOAuth.kt$TwitterOAuth$401</ID> <ID>MagicNumber:TwitterOAuth.kt$TwitterOAuth$401</ID>
<ID>MagicNumber:Users.kt$Users$8</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$1.60934</ID>
<ID>MagicNumber:Weather2.kt$Weather2.Companion$32</ID> <ID>MagicNumber:Weather2.kt$Weather2.Companion$32</ID>
<ID>MagicNumber:Weather2.kt$Weather2.Companion$404</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>ReturnCount:ExceptionSanitizer.kt$ExceptionSanitizer$fun ModuleException.sanitize(vararg sanitize: String): ModuleException</ID>
<ID>SwallowedException:GoogleSearchTest.kt$GoogleSearchTest$e: ModuleException</ID> <ID>SwallowedException:GoogleSearchTest.kt$GoogleSearchTest$e: ModuleException</ID>
<ID>SwallowedException:StockQuoteTest.kt$StockQuoteTest$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>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: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$@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: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: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:StockQuote.kt$StockQuote.Companion$e: NullPointerException</ID>
<ID>TooGenericExceptionCaught:Weather2.kt$Weather2.Companion$e: NullPointerException</ID> <ID>TooGenericExceptionCaught:Weather2.kt$Weather2.Companion$e: NullPointerException</ID>
<ID>TooManyFunctions:Mobibot.kt$Mobibot : ListenerAdapter</ID> <ID>TooManyFunctions:Mobibot.kt$Mobibot : ListenerAdapter</ID>

View file

@ -65,7 +65,7 @@ tell-max-size=50
#alphavantage-api-key= #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 #wolfram-units=imperial

View file

@ -41,11 +41,10 @@ import org.pircbotx.hooks.types.GenericMessageEvent
import org.slf4j.Logger import org.slf4j.Logger
import java.io.BufferedInputStream import java.io.BufferedInputStream
import java.io.BufferedOutputStream import java.io.BufferedOutputStream
import java.io.BufferedReader
import java.io.IOException import java.io.IOException
import java.io.InputStreamReader
import java.io.ObjectInputStream import java.io.ObjectInputStream
import java.io.ObjectOutputStream import java.io.ObjectOutputStream
import java.net.HttpURLConnection
import java.net.URL import java.net.URL
import java.net.URLEncoder import java.net.URLEncoder
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
@ -56,7 +55,6 @@ import java.time.ZoneId
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import java.util.Date import java.util.Date
import java.util.Properties import java.util.Properties
import java.util.stream.Collectors
import kotlin.io.path.exists import kotlin.io.path.exists
import kotlin.io.path.fileSize import kotlin.io.path.fileSize
@ -186,6 +184,12 @@ object Utils {
return event.bot().userChannelDao.getChannel(channel).isOp(event.user) 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. * Returns the last item of a list of strings or empty if none.
*/ */
@ -402,8 +406,21 @@ object Utils {
*/ */
@JvmStatic @JvmStatic
@Throws(IOException::class) @Throws(IOException::class)
fun URL.reader(): String { fun URL.reader(): UrlReaderResponse {
BufferedReader(InputStreamReader(this.openStream(), StandardCharsets.UTF_8)) val connection = this.openConnection() as HttpURLConnection
.use { reader -> return reader.lines().collect(Collectors.joining(System.lineSeparator())) } 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) { } catch (e: CryptoException) {
if (logger.isWarnEnabled) logger.warn("$debugMessage => ${e.statusCode}", e) if (logger.isWarnEnabled) logger.warn("$debugMessage => ${e.statusCode}", e)
e.message?.let { e.message?.let {
event.sendMessage(it) event.respond(it)
} }
} catch (e: IOException) { } catch (e: IOException) {
if (logger.isErrorEnabled) logger.error(debugMessage, e) 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 { } else {
helpResponse(event) helpResponse(event)

View file

@ -143,7 +143,7 @@ class CurrencyConverter : ThreadedModule() {
try { try {
val amt = cmds[0].replace(",", "") val amt = cmds[0].replace(",", "")
val url = URL("https://api.exchangerate.host/convert?from=$to&to=$from&amount=$amt") 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")) { if (json.getBoolean("success")) {
PublicMessage( PublicMessage(
@ -170,7 +170,7 @@ class CurrencyConverter : ThreadedModule() {
fun loadSymbols() { fun loadSymbols() {
try { try {
val url = URL("https://api.exchangerate.host/symbols") val url = URL("https://api.exchangerate.host/symbols")
val json = JSONObject(url.reader()) val json = JSONObject(url.reader().body)
if (json.getBoolean("success")) { if (json.getBoolean("success")) {
val symbols = json.getJSONObject("symbols") val symbols = json.getJSONObject("symbols")
for (key in symbols.keys()) { for (key in symbols.keys()) {

View file

@ -76,7 +76,7 @@ class GoogleSearch : ThreadedModule() {
} catch (e: ModuleException) { } catch (e: ModuleException) {
if (logger.isWarnEnabled) logger.warn(e.debugMessage, e) if (logger.isWarnEnabled) logger.warn(e.debugMessage, e)
e.message?.let { e.message?.let {
event.sendMessage(it) event.respond(it)
} }
} }
} else { } else {
@ -118,7 +118,7 @@ class GoogleSearch : ThreadedModule() {
"https://www.googleapis.com/customsearch/v1?key=$apiKey&cx=$cseKey" + "https://www.googleapis.com/customsearch/v1?key=$apiKey&cx=$cseKey" +
"&quotaUser=${quotaUser}&q=${query.encodeUrl()}&filter=1&num=5&alt=json" "&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")) { if (json.has("items")) {
val ja = json.getJSONArray("items") val ja = json.getJSONArray("items")
for (i in 0 until ja.length()) { for (i in 0 until ja.length()) {
@ -126,6 +126,10 @@ class GoogleSearch : ThreadedModule() {
results.add(NoticeMessage(j.getString("title").unescapeXml())) results.add(NoticeMessage(j.getString("title").unescapeXml()))
results.add(NoticeMessage(helpFormat(j.getString("link"), false), Colors.DARK_GREEN)) 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 { } else {
results.add(ErrorMessage("No results found.", Colors.RED)) results.add(ErrorMessage("No results found.", Colors.RED))
} }

View file

@ -70,7 +70,7 @@ class StockQuote : ThreadedModule() {
} catch (e: ModuleException) { } catch (e: ModuleException) {
if (logger.isWarnEnabled) logger.warn(e.debugMessage, e) if (logger.isWarnEnabled) logger.warn(e.debugMessage, e)
e.message?.let { e.message?.let {
event.sendMessage(it) event.respond(it)
} }
} }
} else { } else {
@ -147,7 +147,7 @@ class StockQuote : ThreadedModule() {
response = URL( response = URL(
"${ALPHAVANTAGE_URL}SYMBOL_SEARCH&keywords=" + symbol.encodeUrl() + "&apikey=" "${ALPHAVANTAGE_URL}SYMBOL_SEARCH&keywords=" + symbol.encodeUrl() + "&apikey="
+ apiKey.encodeUrl() + apiKey.encodeUrl()
).reader() ).reader().body
var json = getJsonResponse(response, debugMessage) var json = getJsonResponse(response, debugMessage)
val symbols = json.getJSONArray("bestMatches") val symbols = json.getJSONArray("bestMatches")
if (symbols.isEmpty) { if (symbols.isEmpty) {
@ -160,7 +160,7 @@ class StockQuote : ThreadedModule() {
"${ALPHAVANTAGE_URL}GLOBAL_QUOTE&symbol=" "${ALPHAVANTAGE_URL}GLOBAL_QUOTE&symbol="
+ symbolInfo.getString("1. symbol").encodeUrl() + "&apikey=" + symbolInfo.getString("1. symbol").encodeUrl() + "&apikey="
+ apiKey.encodeUrl() + apiKey.encodeUrl()
).reader() ).reader().body
json = getJsonResponse(response, debugMessage) json = getJsonResponse(response, debugMessage)
val quote = json.getJSONObject("Global Quote") val quote = json.getJSONObject("Global Quote")
if (quote.isEmpty) { 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
import net.thauvin.erik.mobibot.Utils.encodeUrl 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.reader
import net.thauvin.erik.mobibot.Utils.sendMessage import net.thauvin.erik.mobibot.Utils.sendMessage
import org.pircbotx.hooks.types.GenericMessageEvent import org.pircbotx.hooks.types.GenericMessageEvent
@ -68,13 +69,13 @@ class WolframAlpha : ThreadedModule() {
} else { } else {
getUnits(properties[WOLFRAM_UNITS_PROP]) getUnits(properties[WOLFRAM_UNITS_PROP])
}, },
apiKey = properties[WOLFRAM_API_KEY_PROP] appId = properties[WOLFRAM_APPID_KEY]
) )
) )
} catch (e: ModuleException) { } catch (e: ModuleException) {
if (logger.isWarnEnabled) logger.warn(e.debugMessage, e) if (logger.isWarnEnabled) logger.warn(e.debugMessage, e)
e.message?.let { e.message?.let {
event.sendMessage(it) event.respond(it)
} }
} }
} else { } else {
@ -86,7 +87,7 @@ class WolframAlpha : ThreadedModule() {
/** /**
* The Wolfram Alpha API Key property. * 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 * The Wolfram units properties
@ -103,14 +104,23 @@ class WolframAlpha : ThreadedModule() {
@JvmStatic @JvmStatic
@Throws(ModuleException::class) @Throws(ModuleException::class)
fun queryWolfram(query: String, units: String = IMPERIAL, apiKey: String?): String { fun queryWolfram(query: String, units: String = IMPERIAL, appId: String?): String {
if (!apiKey.isNullOrEmpty()) { if (!appId.isNullOrEmpty()) {
try { 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) { } catch (ioe: IOException) {
throw ModuleException( throw ModuleException(
"wolfram($query): IOE", "wolfram($query): IOE", "An IO Error occurred while querying Wolfram Alpha.", ioe
"Looks like Wolfram Alpha isn't able to answer that.",
) )
} }
} else { } else {
@ -128,6 +138,6 @@ class WolframAlpha : ThreadedModule() {
add(Utils.helpFormat("%c $WOLFRAM_CMD days until christmas")) add(Utils.helpFormat("%c $WOLFRAM_CMD days until christmas"))
add(Utils.helpFormat("%c $WOLFRAM_CMD distance earth moon units=metric")) 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 { private fun validatePin(apiToken: String, url: String, vararg matches: String): Boolean {
val response = 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 { matches.forEach {
if (!response.contains(it)) { if (!response.contains(it)) {

View file

@ -267,7 +267,7 @@ class UtilsTest {
@Test @Test
@Throws(IOException::class) @Throws(IOException::class)
fun testUrlReader() { 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}") .isEqualTo("{\"status\":200}")
} }

View file

@ -34,6 +34,7 @@ package net.thauvin.erik.mobibot.modules
import assertk.all import assertk.all
import assertk.assertThat import assertk.assertThat
import assertk.assertions.contains import assertk.assertions.contains
import assertk.assertions.hasMessage
import assertk.assertions.hasNoCause import assertk.assertions.hasNoCause
import assertk.assertions.isEqualTo import assertk.assertions.isEqualTo
import assertk.assertions.isFailure import assertk.assertions.isFailure
@ -63,6 +64,11 @@ class GoogleSearchTest : LocalProperties() {
assertThat { searchGoogle("test", "apiKey", "") }.isFailure() assertThat { searchGoogle("test", "apiKey", "") }.isFailure()
.isInstanceOf(ModuleException::class.java).hasNoCause() .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"]) @Test(groups = ["no-ci", "modules"])

View file

@ -34,6 +34,7 @@ package net.thauvin.erik.mobibot.modules
import assertk.assertThat import assertk.assertThat
import assertk.assertions.contains import assertk.assertions.contains
import assertk.assertions.hasMessage
import assertk.assertions.isFailure import assertk.assertions.isFailure
import assertk.assertions.isInstanceOf import assertk.assertions.isInstanceOf
import net.thauvin.erik.mobibot.ExceptionSanitizer.sanitize import net.thauvin.erik.mobibot.ExceptionSanitizer.sanitize
@ -41,21 +42,29 @@ import net.thauvin.erik.mobibot.LocalProperties
import org.testng.annotations.Test import org.testng.annotations.Test
class WolframAlphaTest : LocalProperties() { 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) @Throws(ModuleException::class)
fun queryWolframTest() { fun queryWolframTest() {
val apiKey = getProperty(WolframAlpha.WOLFRAM_API_KEY_PROP) val apiKey = getProperty(WolframAlpha.WOLFRAM_APPID_KEY)
try { 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( assertThat(
WolframAlpha.queryWolfram("SFO to LAX", WolframAlpha.METRIC, apiKey), WolframAlpha.queryWolfram("SFO to LAX", WolframAlpha.METRIC, apiKey),
"SFO to LA" "SFO to LA"
).contains("kilometers") ).contains("kilometers")
assertThat { WolframAlpha.queryWolfram("1 gallon to liter", apiKey = "") }
.isFailure()
.isInstanceOf(ModuleException::class.java)
} catch (e: ModuleException) { } catch (e: ModuleException) {
// Avoid displaying api key in CI logs // Avoid displaying api key in CI logs
if ("true" == System.getenv("CI")) { if ("true" == System.getenv("CI")) {