Moved to exchangerate.host API for currency conversion
This commit is contained in:
parent
a33b8354b8
commit
070d29c7dd
7 changed files with 77 additions and 121 deletions
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
|
@ -1,5 +1,5 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" project-jdk-name="17" project-jdk-type="JavaSDK" />
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" project-jdk-name="18" project-jdk-type="JavaSDK" />
|
||||||
</project>
|
</project>
|
|
@ -1,9 +1,11 @@
|
||||||
|
# mobibot
|
||||||
|
|
||||||
[](https://opensource.org/licenses/BSD-3-Clause) [](https://sonarcloud.io/summary/new_code?id=ethauvin_mobibot)
|
[](https://opensource.org/licenses/BSD-3-Clause) [](https://sonarcloud.io/summary/new_code?id=ethauvin_mobibot)
|
||||||
[](https://snyk.io/test/github/ethauvin/mobibot?targetFile=build.gradle) [](https://github.com/ethauvin/mobibot/actions/workflows/gradle.yml) [](https://circleci.com/gh/ethauvin/mobibot/tree/master)
|
[](https://snyk.io/test/github/ethauvin/mobibot?targetFile=build.gradle) [](https://github.com/ethauvin/mobibot/actions/workflows/gradle.yml) [](https://circleci.com/gh/ethauvin/mobibot/tree/master)
|
||||||
|
|
||||||
Some very basic instructions:
|
Some very basic instructions:
|
||||||
|
|
||||||
```
|
```sh
|
||||||
{ clone with git or download the ZIP }
|
{ clone with git or download the ZIP }
|
||||||
git clone https://github.com/ethauvin/mobibot.git
|
git clone https://github.com/ethauvin/mobibot.git
|
||||||
|
|
||||||
|
|
20
build.gradle
20
build.gradle
|
@ -5,10 +5,10 @@ plugins {
|
||||||
id 'io.gitlab.arturbosch.detekt' version '1.20.0'
|
id 'io.gitlab.arturbosch.detekt' version '1.20.0'
|
||||||
id 'java'
|
id 'java'
|
||||||
id 'net.thauvin.erik.gradle.semver' version '1.0.4'
|
id 'net.thauvin.erik.gradle.semver' version '1.0.4'
|
||||||
id 'org.jetbrains.kotlin.jvm' version '1.6.21'
|
id 'org.jetbrains.kotlin.jvm' version '1.7.10'
|
||||||
id 'org.jetbrains.kotlin.kapt' version '1.6.21'
|
id 'org.jetbrains.kotlin.kapt' version '1.7.10'
|
||||||
id 'org.jetbrains.kotlinx.kover' version '0.5.0'
|
id 'org.jetbrains.kotlinx.kover' version '0.5.1'
|
||||||
id 'org.sonarqube' version '3.3'
|
id 'org.sonarqube' version '3.4.0.2513'
|
||||||
id 'pmd'
|
id 'pmd'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,8 +27,8 @@ def isNonStable = { String version ->
|
||||||
mainClassName = packageName + '.Mobibot'
|
mainClassName = packageName + '.Mobibot'
|
||||||
|
|
||||||
ext.versions = [
|
ext.versions = [
|
||||||
log4j: '2.17.2',
|
log4j: '2.18.0',
|
||||||
pmd : '6.44.0',
|
pmd : '6.47.0',
|
||||||
]
|
]
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
|
@ -59,7 +59,7 @@ dependencies {
|
||||||
// Kotlin
|
// Kotlin
|
||||||
implementation platform('org.jetbrains.kotlin:kotlin-bom')
|
implementation platform('org.jetbrains.kotlin:kotlin-bom')
|
||||||
implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8'
|
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
|
// Logging
|
||||||
implementation 'org.slf4j:slf4j-api:1.7.36'
|
implementation 'org.slf4j:slf4j-api:1.7.36'
|
||||||
|
@ -68,11 +68,11 @@ dependencies {
|
||||||
implementation "org.apache.logging.log4j:log4j-slf4j-impl:$versions.log4j"
|
implementation "org.apache.logging.log4j:log4j-slf4j-impl:$versions.log4j"
|
||||||
|
|
||||||
implementation 'com.rometools:rome:1.18.0'
|
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.aksingh:owm-japis:2.5.3.0'
|
||||||
implementation 'net.objecthunter:exp4j:0.4.8'
|
implementation 'net.objecthunter:exp4j:0.4.8'
|
||||||
implementation 'org.json:json:20220320'
|
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 'org.twitter4j:twitter4j-core:4.0.7'
|
||||||
|
|
||||||
implementation 'net.thauvin.erik:cryptoprice:0.9.0'
|
implementation 'net.thauvin.erik:cryptoprice:0.9.0'
|
||||||
|
@ -81,7 +81,7 @@ dependencies {
|
||||||
testImplementation 'com.willowtreeapps.assertk:assertk-jvm:0.25'
|
testImplementation 'com.willowtreeapps.assertk:assertk-jvm:0.25'
|
||||||
// testImplementation 'org.mockito.kotlin:mockito-kotlin:4.0.0'
|
// testImplementation 'org.mockito.kotlin:mockito-kotlin:4.0.0'
|
||||||
// testImplementation "org.mockito:mockito-core: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 {
|
test {
|
||||||
|
|
|
@ -13,13 +13,10 @@
|
||||||
<ID>LongParameterList:Twitter.kt$Twitter.Companion$( consumerKey: String?, consumerSecret: String?, token: String?, tokenSecret: String?, handle: String?, message: String, isDm: Boolean )</ID>
|
<ID>LongParameterList:Twitter.kt$Twitter.Companion$( consumerKey: String?, consumerSecret: String?, token: String?, tokenSecret: String?, handle: String?, message: String, isDm: Boolean )</ID>
|
||||||
<ID>MagicNumber:Comment.kt$Comment$3</ID>
|
<ID>MagicNumber:Comment.kt$Comment$3</ID>
|
||||||
<ID>MagicNumber:CurrencyConverter.kt$CurrencyConverter$11</ID>
|
<ID>MagicNumber:CurrencyConverter.kt$CurrencyConverter$11</ID>
|
||||||
<ID>MagicNumber:CurrencyConverter.kt$CurrencyConverter$3</ID>
|
|
||||||
<ID>MagicNumber:CurrencyConverter.kt$CurrencyConverter.Companion$3</ID>
|
<ID>MagicNumber:CurrencyConverter.kt$CurrencyConverter.Companion$3</ID>
|
||||||
<ID>MagicNumber:CurrencyConverter.kt$CurrencyConverter.Companion$4</ID>
|
<ID>MagicNumber:CurrencyConverter.kt$CurrencyConverter.Companion$4</ID>
|
||||||
<ID>MagicNumber:CurrencyConverter.kt$CurrencyConverter.Companion$8</ID>
|
|
||||||
<ID>MagicNumber:Cycle.kt$Cycle$10</ID>
|
<ID>MagicNumber:Cycle.kt$Cycle$10</ID>
|
||||||
<ID>MagicNumber:Cycle.kt$Cycle$1000L</ID>
|
<ID>MagicNumber:Cycle.kt$Cycle$1000L</ID>
|
||||||
<ID>MagicNumber:Dice.kt$Dice$6</ID>
|
|
||||||
<ID>MagicNumber:Ignore.kt$Ignore$8</ID>
|
<ID>MagicNumber:Ignore.kt$Ignore$8</ID>
|
||||||
<ID>MagicNumber:Info.kt$Info.Companion$30</ID>
|
<ID>MagicNumber:Info.kt$Info.Companion$30</ID>
|
||||||
<ID>MagicNumber:Info.kt$Info.Companion$365</ID>
|
<ID>MagicNumber:Info.kt$Info.Companion$365</ID>
|
||||||
|
@ -47,6 +44,7 @@
|
||||||
<ID>NestedBlockDepth:Addons.kt$Addons$ fun add(module: AbstractModule)</ID>
|
<ID>NestedBlockDepth:Addons.kt$Addons$ fun add(module: AbstractModule)</ID>
|
||||||
<ID>NestedBlockDepth:Comment.kt$Comment$override fun commandResponse(channel: String, args: String, event: GenericMessageEvent)</ID>
|
<ID>NestedBlockDepth:Comment.kt$Comment$override fun commandResponse(channel: String, args: String, event: GenericMessageEvent)</ID>
|
||||||
<ID>NestedBlockDepth:CurrencyConverter.kt$CurrencyConverter.Companion$ @JvmStatic fun convertCurrency(query: String): Message</ID>
|
<ID>NestedBlockDepth:CurrencyConverter.kt$CurrencyConverter.Companion$ @JvmStatic fun convertCurrency(query: String): Message</ID>
|
||||||
|
<ID>NestedBlockDepth:CurrencyConverter.kt$CurrencyConverter.Companion$@JvmStatic @Throws(ModuleException::class) fun loadSymbols()</ID>
|
||||||
<ID>NestedBlockDepth:EntryLink.kt$EntryLink$ private fun setTags(tags: List<String?>)</ID>
|
<ID>NestedBlockDepth:EntryLink.kt$EntryLink$ private fun setTags(tags: List<String?>)</ID>
|
||||||
<ID>NestedBlockDepth:FeedsMgr.kt$FeedsMgr.Companion$ @JvmStatic @Throws(IOException::class, FeedException::class) fun loadFeed(entries: Entries, currentFile: String = currentXml): String</ID>
|
<ID>NestedBlockDepth:FeedsMgr.kt$FeedsMgr.Companion$ @JvmStatic @Throws(IOException::class, FeedException::class) fun loadFeed(entries: Entries, currentFile: String = currentXml): String</ID>
|
||||||
<ID>NestedBlockDepth:FeedsMgr.kt$FeedsMgr.Companion$ @JvmStatic fun saveFeed(entries: Entries, currentFile: String = currentXml)</ID>
|
<ID>NestedBlockDepth:FeedsMgr.kt$FeedsMgr.Companion$ @JvmStatic fun saveFeed(entries: Entries, currentFile: String = currentXml)</ID>
|
||||||
|
@ -73,6 +71,5 @@
|
||||||
<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:Tell.kt$Tell : AbstractCommand</ID>
|
<ID>TooManyFunctions:Tell.kt$Tell : AbstractCommand</ID>
|
||||||
<ID>UnusedPrivateMember:Dice.kt$Dice$private val sides = 6</ID>
|
|
||||||
</CurrentIssues>
|
</CurrentIssues>
|
||||||
</SmellBaseline>
|
</SmellBaseline>
|
||||||
|
|
|
@ -31,27 +31,22 @@
|
||||||
*/
|
*/
|
||||||
package net.thauvin.erik.mobibot.modules
|
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.bot
|
||||||
import net.thauvin.erik.mobibot.Utils.helpCmdSyntax
|
import net.thauvin.erik.mobibot.Utils.helpCmdSyntax
|
||||||
import net.thauvin.erik.mobibot.Utils.helpFormat
|
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.sendList
|
||||||
import net.thauvin.erik.mobibot.Utils.sendMessage
|
import net.thauvin.erik.mobibot.Utils.sendMessage
|
||||||
import net.thauvin.erik.mobibot.Utils.today
|
import net.thauvin.erik.mobibot.Utils.today
|
||||||
import net.thauvin.erik.mobibot.msg.ErrorMessage
|
import net.thauvin.erik.mobibot.msg.ErrorMessage
|
||||||
import net.thauvin.erik.mobibot.msg.Message
|
import net.thauvin.erik.mobibot.msg.Message
|
||||||
import net.thauvin.erik.mobibot.msg.PublicMessage
|
import net.thauvin.erik.mobibot.msg.PublicMessage
|
||||||
import org.jdom2.JDOMException
|
import org.json.JSONObject
|
||||||
import org.jdom2.input.SAXBuilder
|
|
||||||
import org.pircbotx.hooks.types.GenericMessageEvent
|
import org.pircbotx.hooks.types.GenericMessageEvent
|
||||||
import org.slf4j.Logger
|
import org.slf4j.Logger
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.text.NumberFormat
|
|
||||||
import java.util.Currency
|
|
||||||
import java.util.Locale
|
|
||||||
import javax.xml.XMLConstants
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The CurrencyConverter module.
|
* The CurrencyConverter module.
|
||||||
|
@ -64,7 +59,7 @@ class CurrencyConverter : ThreadedModule() {
|
||||||
override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
|
override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
|
||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
if (pubDate != today()) {
|
if (pubDate != today()) {
|
||||||
EXCHANGE_RATES.clear()
|
SYMBOLS.clear()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
super.commandResponse(channel, cmd, args, event)
|
super.commandResponse(channel, cmd, args, event)
|
||||||
|
@ -74,52 +69,55 @@ class CurrencyConverter : ThreadedModule() {
|
||||||
* Converts the specified currencies.
|
* Converts the specified currencies.
|
||||||
*/
|
*/
|
||||||
override fun run(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
|
override fun run(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
|
||||||
if (EXCHANGE_RATES.isEmpty()) {
|
if (SYMBOLS.isEmpty()) {
|
||||||
try {
|
try {
|
||||||
loadRates()
|
loadSymbols()
|
||||||
} catch (e: ModuleException) {
|
} catch (e: ModuleException) {
|
||||||
if (logger.isWarnEnabled) logger.warn(e.debugMessage, e)
|
if (logger.isWarnEnabled) logger.warn(e.debugMessage, e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (EXCHANGE_RATES.isEmpty()) {
|
if (SYMBOLS.isEmpty()) {
|
||||||
event.respond(EMPTY_RATE_TABLE)
|
event.respond(EMPTY_SYMBOLS_TABLE)
|
||||||
} else if (args.matches("\\d+([,\\d]+)?(\\.\\d+)? [a-zA-Z]{3}+ to [a-zA-Z]{3}+".toRegex())) {
|
} else if (args.matches("\\d+([,\\d]+)?(\\.\\d+)? [a-zA-Z]{3}+ to [a-zA-Z]{3}+".toRegex())) {
|
||||||
val msg = convertCurrency(args)
|
val msg = convertCurrency(args)
|
||||||
event.respond(msg.msg)
|
event.respond(msg.msg)
|
||||||
if (msg.isError) {
|
if (msg.isError) {
|
||||||
helpResponse(event)
|
helpResponse(event)
|
||||||
}
|
}
|
||||||
} else if (args.contains(CURRENCY_RATES_KEYWORD)) {
|
} else if (args.contains(CURRENCY_SYMBOLS_KEYWORD)) {
|
||||||
event.sendMessage("The reference rates for ${pubDate.bold()} are:")
|
event.sendMessage("The supported currency symbols are: ")
|
||||||
event.sendList(currencyRates(), 3, " ", isIndent = true)
|
event.sendList(ArrayList(SYMBOLS.keys.sorted()), 11, isIndent = true)
|
||||||
} else {
|
} else {
|
||||||
helpResponse(event)
|
helpResponse(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun helpResponse(event: GenericMessageEvent): Boolean {
|
override fun helpResponse(event: GenericMessageEvent): Boolean {
|
||||||
if (EXCHANGE_RATES.isEmpty()) {
|
if (SYMBOLS.isEmpty()) {
|
||||||
try {
|
try {
|
||||||
loadRates()
|
loadSymbols()
|
||||||
} catch (e: ModuleException) {
|
} catch (e: ModuleException) {
|
||||||
if (logger.isWarnEnabled) logger.warn(e.debugMessage, e)
|
if (logger.isWarnEnabled) logger.warn(e.debugMessage, e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (EXCHANGE_RATES.isEmpty()) {
|
if (SYMBOLS.isEmpty()) {
|
||||||
event.sendMessage(EMPTY_RATE_TABLE)
|
event.sendMessage(EMPTY_SYMBOLS_TABLE)
|
||||||
} else {
|
} else {
|
||||||
val nick = event.bot().nick
|
val nick = event.bot().nick
|
||||||
event.sendMessage("To convert from one currency to another:")
|
event.sendMessage("To convert from one currency to another:")
|
||||||
event.sendMessage(helpFormat(helpCmdSyntax("%c $CURRENCY_CMD 100 USD to EUR", nick, isPrivateMsgEnabled)))
|
event.sendMessage(helpFormat(helpCmdSyntax("%c $CURRENCY_CMD 100 USD to EUR", nick, isPrivateMsgEnabled)))
|
||||||
event.sendMessage("For a listing of current reference rates:")
|
|
||||||
event.sendMessage(
|
event.sendMessage(
|
||||||
helpFormat(
|
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
|
return true
|
||||||
}
|
}
|
||||||
|
@ -128,27 +126,18 @@ class CurrencyConverter : ThreadedModule() {
|
||||||
// Currency command
|
// Currency command
|
||||||
private const val CURRENCY_CMD = "currency"
|
private const val CURRENCY_CMD = "currency"
|
||||||
|
|
||||||
// Rates keyword
|
// Currency synbols keywords
|
||||||
private const val CURRENCY_RATES_KEYWORD = "rates"
|
private const val CURRENCY_SYMBOLS_KEYWORD = "symbols"
|
||||||
|
|
||||||
// Empty rate table.
|
// Empty symbols table.
|
||||||
private const val EMPTY_RATE_TABLE = "Sorry, but the exchange rate table is empty."
|
private const val EMPTY_SYMBOLS_TABLE = "Sorry, but the currency symbols table is empty."
|
||||||
|
|
||||||
// Exchange rates
|
// Currency Symbols
|
||||||
private val EXCHANGE_RATES: MutableMap<String, String> = mutableMapOf()
|
private val SYMBOLS: MutableMap<String, String> = mutableMapOf()
|
||||||
|
|
||||||
// Exchange rates table URL
|
|
||||||
private const val EXCHANGE_TABLE_URL = "https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml"
|
|
||||||
|
|
||||||
// Last exchange rates table publication date
|
// Last exchange rates table publication date
|
||||||
private var pubDate = ""
|
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.
|
* Converts from a currency to another.
|
||||||
*/
|
*/
|
||||||
|
@ -161,17 +150,21 @@ class CurrencyConverter : ThreadedModule() {
|
||||||
} else {
|
} else {
|
||||||
val to = cmds[1].uppercase()
|
val to = cmds[1].uppercase()
|
||||||
val from = cmds[3].uppercase()
|
val from = cmds[3].uppercase()
|
||||||
val toRate = EXCHANGE_RATES[to]
|
if (SYMBOLS.contains(to) && SYMBOLS.contains(from)) {
|
||||||
val fromRate = EXCHANGE_RATES[from]
|
|
||||||
if (!toRate.isNullOrBlank() && !fromRate.isNullOrBlank()) {
|
|
||||||
try {
|
try {
|
||||||
val amt = cmds[0].replace(",", "").toDouble()
|
val amt = cmds[0].replace(",", "")
|
||||||
PublicMessage(
|
val url = URL("https://api.exchangerate.host/convert?from=$to&to=$from&amount=$amt")
|
||||||
amt.formatCurrency(to) + " = "
|
val json = JSONObject(url.reader())
|
||||||
+ (amt * toRate.toDouble() / fromRate.toDouble()).formatCurrency(from)
|
|
||||||
)
|
if (json.getBoolean("success")) {
|
||||||
} catch (e: NumberFormatException) {
|
PublicMessage(
|
||||||
ErrorMessage("Let's try with some real numbers next time, okay?")
|
"${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 {
|
} else {
|
||||||
ErrorMessage("Sounds like monopoly money to me!")
|
ErrorMessage("Sounds like monopoly money to me!")
|
||||||
|
@ -180,48 +173,23 @@ class CurrencyConverter : ThreadedModule() {
|
||||||
} else ErrorMessage("Invalid query. Let's try again.")
|
} else ErrorMessage("Invalid query. Let's try again.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun currencyRates(): List<String> {
|
|
||||||
val rates = buildList {
|
|
||||||
for ((key, value) in EXCHANGE_RATES.toSortedMap()) {
|
|
||||||
add("$key: ${value.padStart(8)}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return rates
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
@Throws(ModuleException::class)
|
@Throws(ModuleException::class)
|
||||||
fun loadRates() {
|
fun loadSymbols() {
|
||||||
if (EXCHANGE_RATES.isEmpty()) {
|
if (SYMBOLS.isEmpty()) {
|
||||||
try {
|
try {
|
||||||
val builder = SAXBuilder()
|
val url = URL("https://api.exchangerate.host/symbols")
|
||||||
// See https://rules.sonarsourcecom/java/tag/owasp/RSPEC-2755
|
val json = JSONObject(url.reader())
|
||||||
builder.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "")
|
if (json.getBoolean("success")) {
|
||||||
builder.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "")
|
val symbols = json.getJSONObject("symbols")
|
||||||
builder.ignoringElementContentWhitespace = true
|
for (key in symbols.keys()) {
|
||||||
val doc = builder.build(URL(EXCHANGE_TABLE_URL))
|
SYMBOLS[key] = symbols.getJSONObject(key).getString("description")
|
||||||
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
|
|
||||||
}
|
}
|
||||||
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) {
|
} catch (e: IOException) {
|
||||||
throw ModuleException(
|
throw ModuleException(
|
||||||
"loadRates(): IOE",
|
"loadSymbols(): IOE",
|
||||||
"An IO error has occurred while parsing the exchange rates table.",
|
"An IO error has occurred while retrieving the currency symbols table.",
|
||||||
e
|
e
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,16 +33,12 @@ package net.thauvin.erik.mobibot.modules
|
||||||
|
|
||||||
import assertk.all
|
import assertk.all
|
||||||
import assertk.assertThat
|
import assertk.assertThat
|
||||||
import assertk.assertions.any
|
|
||||||
import assertk.assertions.contains
|
import assertk.assertions.contains
|
||||||
import assertk.assertions.isGreaterThan
|
|
||||||
import assertk.assertions.isInstanceOf
|
import assertk.assertions.isInstanceOf
|
||||||
import assertk.assertions.matches
|
import assertk.assertions.matches
|
||||||
import assertk.assertions.prop
|
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.convertCurrency
|
||||||
import net.thauvin.erik.mobibot.modules.CurrencyConverter.Companion.currencyRates
|
import net.thauvin.erik.mobibot.modules.CurrencyConverter.Companion.loadSymbols
|
||||||
import net.thauvin.erik.mobibot.modules.CurrencyConverter.Companion.loadRates
|
|
||||||
import net.thauvin.erik.mobibot.msg.ErrorMessage
|
import net.thauvin.erik.mobibot.msg.ErrorMessage
|
||||||
import net.thauvin.erik.mobibot.msg.Message
|
import net.thauvin.erik.mobibot.msg.Message
|
||||||
import net.thauvin.erik.mobibot.msg.PublicMessage
|
import net.thauvin.erik.mobibot.msg.PublicMessage
|
||||||
|
@ -56,7 +52,7 @@ class CurrencyConverterTest {
|
||||||
@BeforeClass
|
@BeforeClass
|
||||||
@Throws(ModuleException::class)
|
@Throws(ModuleException::class)
|
||||||
fun before() {
|
fun before() {
|
||||||
loadRates()
|
loadSymbols()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -64,7 +60,11 @@ class CurrencyConverterTest {
|
||||||
assertThat(
|
assertThat(
|
||||||
convertCurrency("100 USD to EUR").msg,
|
convertCurrency("100 USD to EUR").msg,
|
||||||
"100 USD to EUR"
|
"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 {
|
assertThat(convertCurrency("100 USD to USD"), "100 USD to USD").all {
|
||||||
prop(Message::msg).contains("You're kidding, right?")
|
prop(Message::msg).contains("You're kidding, right?")
|
||||||
isInstanceOf(PublicMessage::class.java)
|
isInstanceOf(PublicMessage::class.java)
|
||||||
|
@ -74,15 +74,4 @@ class CurrencyConverterTest {
|
||||||
isInstanceOf(ErrorMessage::class.java)
|
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")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
#Generated by the Semver Plugin for Gradle
|
#Generated by the Semver Plugin for Gradle
|
||||||
#Tue Apr 19 17:22:30 PDT 2022
|
#Sun Jul 10 14:02:30 PDT 2022
|
||||||
version.buildmeta=221
|
version.buildmeta=257
|
||||||
version.major=0
|
version.major=0
|
||||||
version.minor=8
|
version.minor=8
|
||||||
version.patch=0
|
version.patch=0
|
||||||
version.prerelease=rc
|
version.prerelease=rc
|
||||||
version.project=mobibot
|
version.project=mobibot
|
||||||
version.semver=0.8.0-rc+221
|
version.semver=0.8.0-rc+257
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue