From 070d29c7ddb964507d876b3523e042a7a5da1b13 Mon Sep 17 00:00:00 2001 From: "Erik C. Thauvin" Date: Sun, 10 Jul 2022 13:53:32 -0700 Subject: [PATCH] Moved to exchangerate.host API for currency conversion --- .idea/misc.xml | 2 +- README.md | 4 +- build.gradle | 20 +-- config/detekt/baseline.xml | 5 +- .../erik/mobibot/modules/CurrencyConverter.kt | 136 +++++++----------- .../mobibot/modules/CurrencyConverterTest.kt | 25 +--- version.properties | 6 +- 7 files changed, 77 insertions(+), 121 deletions(-) diff --git a/.idea/misc.xml b/.idea/misc.xml index cbfe0de..fec46c9 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/README.md b/README.md index 91b6a46..91ae8bf 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,11 @@ +# mobibot + [![License (3-Clause BSD)](https://img.shields.io/badge/license-BSD%203--Clause-blue.svg)](https://opensource.org/licenses/BSD-3-Clause) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=ethauvin_mobibot&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=ethauvin_mobibot) [![Known Vulnerabilities](https://snyk.io/test/github/ethauvin/mobibot/badge.svg?targetFile=build.gradle)](https://snyk.io/test/github/ethauvin/mobibot?targetFile=build.gradle) [![GitHub CI](https://github.com/ethauvin/mobibot/actions/workflows/gradle.yml/badge.svg)](https://github.com/ethauvin/mobibot/actions/workflows/gradle.yml) [![CircleCI](https://circleci.com/gh/ethauvin/mobibot/tree/master.svg?style=shield)](https://circleci.com/gh/ethauvin/mobibot/tree/master) Some very basic instructions: -``` +```sh { clone with git or download the ZIP } git clone https://github.com/ethauvin/mobibot.git diff --git a/build.gradle b/build.gradle index 5ff9a71..7765eb8 100644 --- a/build.gradle +++ b/build.gradle @@ -5,10 +5,10 @@ plugins { id 'io.gitlab.arturbosch.detekt' version '1.20.0' id 'java' id 'net.thauvin.erik.gradle.semver' version '1.0.4' - id 'org.jetbrains.kotlin.jvm' version '1.6.21' - id 'org.jetbrains.kotlin.kapt' version '1.6.21' - id 'org.jetbrains.kotlinx.kover' version '0.5.0' - id 'org.sonarqube' version '3.3' + id 'org.jetbrains.kotlin.jvm' version '1.7.10' + id 'org.jetbrains.kotlin.kapt' version '1.7.10' + id 'org.jetbrains.kotlinx.kover' version '0.5.1' + id 'org.sonarqube' version '3.4.0.2513' id 'pmd' } @@ -27,8 +27,8 @@ def isNonStable = { String version -> mainClassName = packageName + '.Mobibot' ext.versions = [ - log4j: '2.17.2', - pmd : '6.44.0', + log4j: '2.18.0', + pmd : '6.47.0', ] repositories { @@ -59,7 +59,7 @@ dependencies { // Kotlin implementation platform('org.jetbrains.kotlin:kotlin-bom') implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.1' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.3' // Logging implementation 'org.slf4j:slf4j-api:1.7.36' @@ -68,11 +68,11 @@ dependencies { implementation "org.apache.logging.log4j:log4j-slf4j-impl:$versions.log4j" implementation 'com.rometools:rome:1.18.0' - implementation 'com.squareup.okhttp3:okhttp:4.9.3' + implementation 'com.squareup.okhttp3:okhttp:4.10.0' implementation 'net.aksingh:owm-japis:2.5.3.0' implementation 'net.objecthunter:exp4j:0.4.8' implementation 'org.json:json:20220320' - implementation 'org.jsoup:jsoup:1.14.3' + implementation 'org.jsoup:jsoup:1.15.2' implementation 'org.twitter4j:twitter4j-core:4.0.7' implementation 'net.thauvin.erik:cryptoprice:0.9.0' @@ -81,7 +81,7 @@ dependencies { testImplementation 'com.willowtreeapps.assertk:assertk-jvm:0.25' // testImplementation 'org.mockito.kotlin:mockito-kotlin:4.0.0' // testImplementation "org.mockito:mockito-core:4.0.0" - testImplementation 'org.testng:testng:7.5' + testImplementation 'org.testng:testng:7.6.1' } test { diff --git a/config/detekt/baseline.xml b/config/detekt/baseline.xml index fa308ea..565e84b 100644 --- a/config/detekt/baseline.xml +++ b/config/detekt/baseline.xml @@ -13,13 +13,10 @@ LongParameterList:Twitter.kt$Twitter.Companion$( consumerKey: String?, consumerSecret: String?, token: String?, tokenSecret: String?, handle: String?, message: String, isDm: Boolean ) MagicNumber:Comment.kt$Comment$3 MagicNumber:CurrencyConverter.kt$CurrencyConverter$11 - MagicNumber:CurrencyConverter.kt$CurrencyConverter$3 MagicNumber:CurrencyConverter.kt$CurrencyConverter.Companion$3 MagicNumber:CurrencyConverter.kt$CurrencyConverter.Companion$4 - MagicNumber:CurrencyConverter.kt$CurrencyConverter.Companion$8 MagicNumber:Cycle.kt$Cycle$10 MagicNumber:Cycle.kt$Cycle$1000L - MagicNumber:Dice.kt$Dice$6 MagicNumber:Ignore.kt$Ignore$8 MagicNumber:Info.kt$Info.Companion$30 MagicNumber:Info.kt$Info.Companion$365 @@ -47,6 +44,7 @@ NestedBlockDepth:Addons.kt$Addons$ fun add(module: AbstractModule) NestedBlockDepth:Comment.kt$Comment$override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) NestedBlockDepth:CurrencyConverter.kt$CurrencyConverter.Companion$ @JvmStatic fun convertCurrency(query: String): Message + NestedBlockDepth:CurrencyConverter.kt$CurrencyConverter.Companion$@JvmStatic @Throws(ModuleException::class) fun loadSymbols() NestedBlockDepth:EntryLink.kt$EntryLink$ private fun setTags(tags: List<String?>) NestedBlockDepth:FeedsMgr.kt$FeedsMgr.Companion$ @JvmStatic @Throws(IOException::class, FeedException::class) fun loadFeed(entries: Entries, currentFile: String = currentXml): String NestedBlockDepth:FeedsMgr.kt$FeedsMgr.Companion$ @JvmStatic fun saveFeed(entries: Entries, currentFile: String = currentXml) @@ -73,6 +71,5 @@ TooGenericExceptionCaught:StockQuote.kt$StockQuote.Companion$e: NullPointerException TooGenericExceptionCaught:Weather2.kt$Weather2.Companion$e: NullPointerException TooManyFunctions:Tell.kt$Tell : AbstractCommand - UnusedPrivateMember:Dice.kt$Dice$private val sides = 6 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 e597df7..0afd499 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/modules/CurrencyConverter.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/CurrencyConverter.kt @@ -31,27 +31,22 @@ */ package net.thauvin.erik.mobibot.modules -import net.thauvin.erik.mobibot.Utils.bold import net.thauvin.erik.mobibot.Utils.bot import net.thauvin.erik.mobibot.Utils.helpCmdSyntax import net.thauvin.erik.mobibot.Utils.helpFormat +import net.thauvin.erik.mobibot.Utils.reader import net.thauvin.erik.mobibot.Utils.sendList import net.thauvin.erik.mobibot.Utils.sendMessage import net.thauvin.erik.mobibot.Utils.today import net.thauvin.erik.mobibot.msg.ErrorMessage import net.thauvin.erik.mobibot.msg.Message import net.thauvin.erik.mobibot.msg.PublicMessage -import org.jdom2.JDOMException -import org.jdom2.input.SAXBuilder +import org.json.JSONObject import org.pircbotx.hooks.types.GenericMessageEvent import org.slf4j.Logger import org.slf4j.LoggerFactory import java.io.IOException import java.net.URL -import java.text.NumberFormat -import java.util.Currency -import java.util.Locale -import javax.xml.XMLConstants /** * The CurrencyConverter module. @@ -64,7 +59,7 @@ class CurrencyConverter : ThreadedModule() { override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) { synchronized(this) { if (pubDate != today()) { - EXCHANGE_RATES.clear() + SYMBOLS.clear() } } super.commandResponse(channel, cmd, args, event) @@ -74,52 +69,55 @@ class CurrencyConverter : ThreadedModule() { * Converts the specified currencies. */ override fun run(channel: String, cmd: String, args: String, event: GenericMessageEvent) { - if (EXCHANGE_RATES.isEmpty()) { + if (SYMBOLS.isEmpty()) { try { - loadRates() + loadSymbols() } catch (e: ModuleException) { if (logger.isWarnEnabled) logger.warn(e.debugMessage, e) } } - if (EXCHANGE_RATES.isEmpty()) { - event.respond(EMPTY_RATE_TABLE) + if (SYMBOLS.isEmpty()) { + event.respond(EMPTY_SYMBOLS_TABLE) } else if (args.matches("\\d+([,\\d]+)?(\\.\\d+)? [a-zA-Z]{3}+ to [a-zA-Z]{3}+".toRegex())) { val msg = convertCurrency(args) event.respond(msg.msg) if (msg.isError) { helpResponse(event) } - } else if (args.contains(CURRENCY_RATES_KEYWORD)) { - event.sendMessage("The reference rates for ${pubDate.bold()} are:") - event.sendList(currencyRates(), 3, " ", isIndent = true) + } else if (args.contains(CURRENCY_SYMBOLS_KEYWORD)) { + event.sendMessage("The supported currency symbols are: ") + event.sendList(ArrayList(SYMBOLS.keys.sorted()), 11, isIndent = true) } else { helpResponse(event) } } override fun helpResponse(event: GenericMessageEvent): Boolean { - if (EXCHANGE_RATES.isEmpty()) { + if (SYMBOLS.isEmpty()) { try { - loadRates() + loadSymbols() } catch (e: ModuleException) { if (logger.isWarnEnabled) logger.warn(e.debugMessage, e) } } - if (EXCHANGE_RATES.isEmpty()) { - event.sendMessage(EMPTY_RATE_TABLE) + if (SYMBOLS.isEmpty()) { + event.sendMessage(EMPTY_SYMBOLS_TABLE) } else { val nick = event.bot().nick event.sendMessage("To convert from one currency to another:") event.sendMessage(helpFormat(helpCmdSyntax("%c $CURRENCY_CMD 100 USD to EUR", nick, isPrivateMsgEnabled))) - event.sendMessage("For a listing of current reference rates:") event.sendMessage( helpFormat( - helpCmdSyntax("%c $CURRENCY_CMD $CURRENCY_RATES_KEYWORD", nick, isPrivateMsgEnabled) + helpCmdSyntax("%c $CURRENCY_CMD 50,000 GBP to BTC", nick, isPrivateMsgEnabled) + ) + ) + event.sendMessage("To list the supported currency symbols: ") + event.sendMessage( + helpFormat( + helpCmdSyntax("%c $CURRENCY_CMD $CURRENCY_SYMBOLS_KEYWORD", nick, isPrivateMsgEnabled) ) ) - event.sendMessage("The supported currencies are: ") - event.sendList(ArrayList(EXCHANGE_RATES.keys), 11, isIndent = true) } return true } @@ -128,27 +126,18 @@ class CurrencyConverter : ThreadedModule() { // Currency command private const val CURRENCY_CMD = "currency" - // Rates keyword - private const val CURRENCY_RATES_KEYWORD = "rates" + // Currency synbols keywords + private const val CURRENCY_SYMBOLS_KEYWORD = "symbols" - // Empty rate table. - private const val EMPTY_RATE_TABLE = "Sorry, but the exchange rate table is empty." + // Empty symbols table. + private const val EMPTY_SYMBOLS_TABLE = "Sorry, but the currency symbols table is empty." - // Exchange rates - private val EXCHANGE_RATES: MutableMap = mutableMapOf() - - // Exchange rates table URL - private const val EXCHANGE_TABLE_URL = "https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml" + // Currency Symbols + private val SYMBOLS: MutableMap = mutableMapOf() // Last exchange rates table publication date private var pubDate = "" - private fun Double.formatCurrency(currency: String): String = - NumberFormat.getCurrencyInstance(Locale.getDefault(Locale.Category.FORMAT)).let { - it.currency = Currency.getInstance(currency) - it.format(this) - } - /** * Converts from a currency to another. */ @@ -161,17 +150,21 @@ class CurrencyConverter : ThreadedModule() { } else { val to = cmds[1].uppercase() val from = cmds[3].uppercase() - val toRate = EXCHANGE_RATES[to] - val fromRate = EXCHANGE_RATES[from] - if (!toRate.isNullOrBlank() && !fromRate.isNullOrBlank()) { + if (SYMBOLS.contains(to) && SYMBOLS.contains(from)) { try { - val amt = cmds[0].replace(",", "").toDouble() - PublicMessage( - amt.formatCurrency(to) + " = " - + (amt * toRate.toDouble() / fromRate.toDouble()).formatCurrency(from) - ) - } catch (e: NumberFormatException) { - ErrorMessage("Let's try with some real numbers next time, okay?") + val amt = cmds[0].replace(",", "") + val url = URL("https://api.exchangerate.host/convert?from=$to&to=$from&amount=$amt") + val json = JSONObject(url.reader()) + + if (json.getBoolean("success")) { + PublicMessage( + "${cmds[0]} ${SYMBOLS[to]} = ${json.get("result")} ${SYMBOLS[from]}" + ) + } else { + ErrorMessage("Sorry, an error occurred while converting the currencies.") + } + } catch (ignore: IOException) { + ErrorMessage("Sorry, an IO error occurred while converting the currencies.") } } else { ErrorMessage("Sounds like monopoly money to me!") @@ -180,48 +173,23 @@ class CurrencyConverter : ThreadedModule() { } else ErrorMessage("Invalid query. Let's try again.") } - - @JvmStatic - fun currencyRates(): List { - val rates = buildList { - for ((key, value) in EXCHANGE_RATES.toSortedMap()) { - add("$key: ${value.padStart(8)}") - } - } - return rates - } - @JvmStatic @Throws(ModuleException::class) - fun loadRates() { - if (EXCHANGE_RATES.isEmpty()) { + fun loadSymbols() { + if (SYMBOLS.isEmpty()) { try { - val builder = SAXBuilder() - // See https://rules.sonarsourcecom/java/tag/owasp/RSPEC-2755 - builder.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "") - builder.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "") - builder.ignoringElementContentWhitespace = true - val doc = builder.build(URL(EXCHANGE_TABLE_URL)) - val root = doc.rootElement - val ns = root.getNamespace("") - val cubeRoot = root.getChild("Cube", ns) - val cubeTime = cubeRoot.getChild("Cube", ns) - pubDate = cubeTime.getAttribute("time").value - val cubes = cubeTime.children - for (cube in cubes) { - EXCHANGE_RATES[cube.getAttribute("currency").value] = cube.getAttribute("rate").value + val url = URL("https://api.exchangerate.host/symbols") + val json = JSONObject(url.reader()) + if (json.getBoolean("success")) { + val symbols = json.getJSONObject("symbols") + for (key in symbols.keys()) { + SYMBOLS[key] = symbols.getJSONObject(key).getString("description") + } } - EXCHANGE_RATES["EUR"] = "1" - } catch (e: JDOMException) { - throw ModuleException( - "loadRates(): JDOM", - "An JDOM parsing error has occurred while parsing the exchange rates table.", - e - ) } catch (e: IOException) { throw ModuleException( - "loadRates(): IOE", - "An IO error has occurred while parsing the exchange rates table.", + "loadSymbols(): IOE", + "An IO error has occurred while retrieving the currency symbols table.", e ) } diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/modules/CurrencyConverterTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/modules/CurrencyConverterTest.kt index 468a947..a0f4ce4 100644 --- a/src/test/kotlin/net/thauvin/erik/mobibot/modules/CurrencyConverterTest.kt +++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/CurrencyConverterTest.kt @@ -33,16 +33,12 @@ package net.thauvin.erik.mobibot.modules import assertk.all import assertk.assertThat -import assertk.assertions.any import assertk.assertions.contains -import assertk.assertions.isGreaterThan import assertk.assertions.isInstanceOf import assertk.assertions.matches import assertk.assertions.prop -import assertk.assertions.size import net.thauvin.erik.mobibot.modules.CurrencyConverter.Companion.convertCurrency -import net.thauvin.erik.mobibot.modules.CurrencyConverter.Companion.currencyRates -import net.thauvin.erik.mobibot.modules.CurrencyConverter.Companion.loadRates +import net.thauvin.erik.mobibot.modules.CurrencyConverter.Companion.loadSymbols import net.thauvin.erik.mobibot.msg.ErrorMessage import net.thauvin.erik.mobibot.msg.Message import net.thauvin.erik.mobibot.msg.PublicMessage @@ -56,7 +52,7 @@ class CurrencyConverterTest { @BeforeClass @Throws(ModuleException::class) fun before() { - loadRates() + loadSymbols() } @Test @@ -64,7 +60,11 @@ class CurrencyConverterTest { assertThat( convertCurrency("100 USD to EUR").msg, "100 USD to EUR" - ).matches("\\$100\\.00 = €\\d{2,3}\\.\\d{2}".toRegex()) + ).matches("100 United States Dollar = \\d{2,3}\\.\\d+ Euro".toRegex()) + assertThat( + convertCurrency("100,000.00 GBP to BTC").msg, + "100 USD to EUR" + ).matches("100,000.00 British Pound Sterling = \\d{1,2}\\.\\d+ Bitcoin".toRegex()) assertThat(convertCurrency("100 USD to USD"), "100 USD to USD").all { prop(Message::msg).contains("You're kidding, right?") isInstanceOf(PublicMessage::class.java) @@ -74,15 +74,4 @@ class CurrencyConverterTest { isInstanceOf(ErrorMessage::class.java) } } - - @Test - fun testCurrencyRates() { - val rates = currencyRates() - assertThat(rates).all { - size().isGreaterThan(30) - any { it.matches("[A-Z]{3}: +[\\d.]+".toRegex()) } - contains("EUR: 1") - } - - } } diff --git a/version.properties b/version.properties index 9129fd0..a7116be 100644 --- a/version.properties +++ b/version.properties @@ -1,9 +1,9 @@ #Generated by the Semver Plugin for Gradle -#Tue Apr 19 17:22:30 PDT 2022 -version.buildmeta=221 +#Sun Jul 10 14:02:30 PDT 2022 +version.buildmeta=257 version.major=0 version.minor=8 version.patch=0 version.prerelease=rc version.project=mobibot -version.semver=0.8.0-rc+221 +version.semver=0.8.0-rc+257