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"?>
|
||||
<project version="4">
|
||||
<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>
|
|
@ -1,9 +1,11 @@
|
|||
# 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)
|
||||
|
||||
Some very basic instructions:
|
||||
|
||||
```
|
||||
```sh
|
||||
{ clone with git or download the ZIP }
|
||||
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 '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 {
|
||||
|
|
|
@ -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>MagicNumber:Comment.kt$Comment$3</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$4</ID>
|
||||
<ID>MagicNumber:CurrencyConverter.kt$CurrencyConverter.Companion$8</ID>
|
||||
<ID>MagicNumber:Cycle.kt$Cycle$10</ID>
|
||||
<ID>MagicNumber:Cycle.kt$Cycle$1000L</ID>
|
||||
<ID>MagicNumber:Dice.kt$Dice$6</ID>
|
||||
<ID>MagicNumber:Ignore.kt$Ignore$8</ID>
|
||||
<ID>MagicNumber:Info.kt$Info.Companion$30</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: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 @Throws(ModuleException::class) fun loadSymbols()</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 fun saveFeed(entries: Entries, currentFile: String = currentXml)</ID>
|
||||
|
@ -73,6 +71,5 @@
|
|||
<ID>TooGenericExceptionCaught:StockQuote.kt$StockQuote.Companion$e: NullPointerException</ID>
|
||||
<ID>TooGenericExceptionCaught:Weather2.kt$Weather2.Companion$e: NullPointerException</ID>
|
||||
<ID>TooManyFunctions:Tell.kt$Tell : AbstractCommand</ID>
|
||||
<ID>UnusedPrivateMember:Dice.kt$Dice$private val sides = 6</ID>
|
||||
</CurrentIssues>
|
||||
</SmellBaseline>
|
||||
|
|
|
@ -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<String, String> = 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<String, String> = 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()
|
||||
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(
|
||||
amt.formatCurrency(to) + " = "
|
||||
+ (amt * toRate.toDouble() / fromRate.toDouble()).formatCurrency(from)
|
||||
"${cmds[0]} ${SYMBOLS[to]} = ${json.get("result")} ${SYMBOLS[from]}"
|
||||
)
|
||||
} catch (e: NumberFormatException) {
|
||||
ErrorMessage("Let's try with some real numbers next time, okay?")
|
||||
} 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<String> {
|
||||
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
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue