Compare commits

...

2 commits

Author SHA1 Message Date
8fb872ad6f Switched to ExchangeRate-API 2023-09-22 03:06:08 -07:00
65fa55d51f Updated dependencies 2023-09-22 03:05:10 -07:00
7 changed files with 80 additions and 50 deletions

2
.idea/kotlinc.xml generated
View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="KotlinJpsPluginSettings"> <component name="KotlinJpsPluginSettings">
<option name="version" value="1.9.0" /> <option name="version" value="1.9.10" />
</component> </component>
</project> </project>

View file

@ -5,7 +5,7 @@ import io.gitlab.arturbosch.detekt.DetektCreateBaselineTask
plugins { plugins {
id 'application' id 'application'
id 'com.github.ben-manes.versions' version '0.47.0' id 'com.github.ben-manes.versions' version '0.48.0'
id 'idea' id 'idea'
id 'io.gitlab.arturbosch.detekt' version '1.23.1' id 'io.gitlab.arturbosch.detekt' version '1.23.1'
id 'java' id 'java'
@ -70,7 +70,7 @@ dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-cli:0.3.6' implementation 'org.jetbrains.kotlinx:kotlinx-cli:0.3.6'
// Logging // Logging
implementation 'org.slf4j:slf4j-api:2.0.7' implementation 'org.slf4j:slf4j-api:2.0.9'
implementation "org.apache.logging.log4j:log4j-api:$versions.log4j" implementation "org.apache.logging.log4j:log4j-api:$versions.log4j"
implementation "org.apache.logging.log4j:log4j-core:$versions.log4j" implementation "org.apache.logging.log4j:log4j-core:$versions.log4j"
implementation "org.apache.logging.log4j:log4j-slf4j2-impl:$versions.log4j" implementation "org.apache.logging.log4j:log4j-slf4j2-impl:$versions.log4j"
@ -86,9 +86,9 @@ dependencies {
implementation 'net.thauvin.erik:cryptoprice:1.0.0' implementation 'net.thauvin.erik:cryptoprice:1.0.0'
implementation 'net.thauvin.erik:jokeapi:0.9-SNAPSHOT' implementation 'net.thauvin.erik:jokeapi:0.9-SNAPSHOT'
implementation 'net.thauvin.erik:pinboard-poster:1.0.3' implementation 'net.thauvin.erik:pinboard-poster:1.0.3'
implementation 'net.thauvin.erik:urlencoder:1.3.0' implementation 'net.thauvin.erik.urlencoder:urlencoder-lib:1.4.0'
testImplementation 'com.willowtreeapps.assertk:assertk-jvm:0.26.1' testImplementation 'com.willowtreeapps.assertk:assertk-jvm:0.27.0'
// 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.8.0' testImplementation 'org.testng:testng:7.8.0'

View file

@ -59,7 +59,8 @@
<ID>NestedBlockDepth:Addons.kt$Addons$fun add(module: AbstractModule): Boolean</ID> <ID>NestedBlockDepth:Addons.kt$Addons$fun add(module: AbstractModule): Boolean</ID>
<ID>NestedBlockDepth:ChatGpt.kt$ChatGpt.Companion$@JvmStatic @Throws(ModuleException::class) fun chat(query: String, apiKey: String?, maxTokens: Int): String</ID> <ID>NestedBlockDepth:ChatGpt.kt$ChatGpt.Companion$@JvmStatic @Throws(ModuleException::class) fun chat(query: String, apiKey: String?, maxTokens: Int): String</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 @Throws(ModuleException::class) fun loadSymbols(apiKey: String?)</ID>
<ID>NestedBlockDepth:CurrencyConverter.kt$CurrencyConverter.Companion$@JvmStatic fun convertCurrency(apiKey: String?, query: String): Message</ID>
<ID>NestedBlockDepth:EntryLink.kt$EntryLink$private fun setTags(tags: List&lt;String?&gt;)</ID> <ID>NestedBlockDepth:EntryLink.kt$EntryLink$private fun setTags(tags: List&lt;String?&gt;)</ID>
<ID>NestedBlockDepth:FeedsManager.kt$FeedsManager.Companion$@JvmStatic @Throws(IOException::class, FeedException::class) fun loadFeed(entries: Entries, currentFile: String = currentXml): String</ID> <ID>NestedBlockDepth:FeedsManager.kt$FeedsManager.Companion$@JvmStatic @Throws(IOException::class, FeedException::class) fun loadFeed(entries: Entries, currentFile: String = currentXml): String</ID>
<ID>NestedBlockDepth:FeedsManager.kt$FeedsManager.Companion$@JvmStatic fun saveFeed(entries: Entries, currentFile: String = currentXml)</ID> <ID>NestedBlockDepth:FeedsManager.kt$FeedsManager.Companion$@JvmStatic fun saveFeed(entries: Entries, currentFile: String = currentXml)</ID>

View file

@ -45,6 +45,12 @@ disabled-modules=mastodon
# Automatically post links to Mastodon # Automatically post links to Mastodon
#mastodon-auto-post=true #mastodon-auto-post=true
#
# Get Exchange Rate API key from: https://www.exchangerate-api.com/
#
#exchangerate-api-key=
# #
# Create custom search engine at: https://programmablesearchengine.google.com/ # Create custom search engine at: https://programmablesearchengine.google.com/
# and get API key from: https://console.cloud.google.com/apis # and get API key from: https://console.cloud.google.com/apis

View file

@ -32,7 +32,7 @@ package net.thauvin.erik.mobibot
import net.thauvin.erik.mobibot.msg.Message import net.thauvin.erik.mobibot.msg.Message
import net.thauvin.erik.mobibot.msg.Message.Companion.DEFAULT_COLOR import net.thauvin.erik.mobibot.msg.Message.Companion.DEFAULT_COLOR
import net.thauvin.erik.urlencoder.UrlEncoder import net.thauvin.erik.urlencoder.UrlEncoderUtil
import org.jsoup.Jsoup import org.jsoup.Jsoup
import org.pircbotx.Colors import org.pircbotx.Colors
import org.pircbotx.PircBotX import org.pircbotx.PircBotX
@ -42,6 +42,7 @@ import org.slf4j.Logger
import java.io.* import java.io.*
import java.net.HttpURLConnection import java.net.HttpURLConnection
import java.net.URL import java.net.URL
import java.net.URLEncoder
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Paths import java.nio.file.Paths
import java.time.LocalDateTime import java.time.LocalDateTime
@ -145,7 +146,7 @@ object Utils {
* URL encodes the given string. * URL encodes the given string.
*/ */
@JvmStatic @JvmStatic
fun String.encodeUrl(): String = UrlEncoder.encode(this) fun String.encodeUrl(): String = UrlEncoderUtil.encode(this)
/** /**
* Returns a property as an int. * Returns a property as an int.

View file

@ -44,6 +44,7 @@ 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.math.BigDecimal
import java.net.URL import java.net.URL
import java.util.* import java.util.*
@ -57,10 +58,10 @@ class CurrencyConverter : AbstractModule() {
override val name = "CurrencyConverter" override val name = "CurrencyConverter"
// Reload currency codes // Reload currency codes
private fun reload() { private fun reload(apiKey: String?) {
if (SYMBOLS.isEmpty()) { if (!apiKey.isNullOrEmpty() && SYMBOLS.isEmpty()) {
try { try {
loadSymbols() loadSymbols(apiKey)
} catch (e: ModuleException) { } catch (e: ModuleException) {
if (logger.isWarnEnabled) logger.warn(e.debugMessage, e) if (logger.isWarnEnabled) logger.warn(e.debugMessage, e)
} }
@ -71,12 +72,12 @@ class CurrencyConverter : AbstractModule() {
* Converts the specified currencies. * Converts the specified currencies.
*/ */
override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) { override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
reload() reload(properties[API_KEY_PROP])
if (SYMBOLS.isEmpty()) { if (SYMBOLS.isEmpty()) {
event.respond(EMPTY_SYMBOLS_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(properties[API_KEY_PROP], args)
event.respond(msg.msg) event.respond(msg.msg)
if (msg.isError) { if (msg.isError) {
helpResponse(event) helpResponse(event)
@ -90,7 +91,7 @@ class CurrencyConverter : AbstractModule() {
} }
override fun helpResponse(event: GenericMessageEvent): Boolean { override fun helpResponse(event: GenericMessageEvent): Boolean {
reload() reload(properties[API_KEY_PROP])
if (SYMBOLS.isEmpty()) { if (SYMBOLS.isEmpty()) {
event.sendMessage(EMPTY_SYMBOLS_TABLE) event.sendMessage(EMPTY_SYMBOLS_TABLE)
@ -99,21 +100,26 @@ class CurrencyConverter : AbstractModule() {
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( event.sendMessage(
helpFormat( helpFormat(
helpCmdSyntax("%c $CURRENCY_CMD 50,000 GBP to BTC", nick, isPrivateMsgEnabled) helpCmdSyntax("%c $CURRENCY_CMD 50,000 GBP to BTC", nick, isPrivateMsgEnabled)
) )
) )
event.sendMessage("To list the supported currency codes:") event.sendMessage("To list the supported currency codes:")
event.sendMessage( event.sendMessage(
helpFormat( helpFormat(
helpCmdSyntax("%c $CURRENCY_CMD $CODES_KEYWORD", nick, isPrivateMsgEnabled) helpCmdSyntax("%c $CURRENCY_CMD $CODES_KEYWORD", nick, isPrivateMsgEnabled)
) )
) )
} }
return true return true
} }
companion object { companion object {
/**
* The API Key property.
*/
const val API_KEY_PROP = "exchangerate-api-key"
// Currency command // Currency command
private const val CURRENCY_CMD = "currency" private const val CURRENCY_CMD = "currency"
@ -130,7 +136,11 @@ class CurrencyConverter : AbstractModule() {
* Converts from a currency to another. * Converts from a currency to another.
*/ */
@JvmStatic @JvmStatic
fun convertCurrency(query: String): Message { fun convertCurrency(apiKey: String?, query: String): Message {
if (apiKey.isNullOrEmpty()) {
throw ModuleException("${CURRENCY_CMD}($query)", "No Exchange Rate API key specified.")
}
val cmds = query.split(" ") val cmds = query.split(" ")
return if (cmds.size == 4) { return if (cmds.size == 4) {
if (cmds[3] == cmds[1] || "0" == cmds[0]) { if (cmds[3] == cmds[1] || "0" == cmds[0]) {
@ -141,12 +151,14 @@ class CurrencyConverter : AbstractModule() {
if (SYMBOLS.contains(to) && SYMBOLS.contains(from)) { if (SYMBOLS.contains(to) && SYMBOLS.contains(from)) {
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://v6.exchangerate-api.com/v6/$apiKey/pair/$to/$from/$amt")
val json = JSONObject(url.reader().body) val body = url.reader().body
val json = JSONObject(body)
if (json.getBoolean("success")) { if (json.getString("result") == "success") {
val result = json.getDouble("conversion_result")
PublicMessage( PublicMessage(
"${cmds[0]} ${SYMBOLS[to]} = ${json.get("result")} ${SYMBOLS[from]}" "${cmds[0]} ${SYMBOLS[to]} = $result ${SYMBOLS[from]}"
) )
} else { } else {
ErrorMessage("Sorry, an error occurred while converting the currencies.") ErrorMessage("Sorry, an error occurred while converting the currencies.")
@ -158,7 +170,9 @@ class CurrencyConverter : AbstractModule() {
ErrorMessage("Sounds like monopoly money to me!") ErrorMessage("Sounds like monopoly money to me!")
} }
} }
} else ErrorMessage("Invalid query. Let's try again.") } else {
ErrorMessage("Invalid query. Let's try again.")
}
} }
/** /**
@ -166,28 +180,32 @@ class CurrencyConverter : AbstractModule() {
*/ */
@JvmStatic @JvmStatic
@Throws(ModuleException::class) @Throws(ModuleException::class)
fun loadSymbols() { fun loadSymbols(apiKey: String?) {
try { if (!apiKey.isNullOrEmpty()) {
val url = URL("https://api.exchangerate.host/symbols") try {
val json = JSONObject(url.reader().body) val url = URL("https://v6.exchangerate-api.com/v6/$apiKey/codes")
if (json.getBoolean("success")) { val json = JSONObject(url.reader().body)
val symbols = json.getJSONObject("symbols") if (json.getString("result") == "success") {
for (key in symbols.keys()) { val codes = json.getJSONArray("supported_codes")
SYMBOLS[key] = symbols.getJSONObject(key).getString("description") for (i in 0 until codes.length()) {
val code = codes.getJSONArray(i);
SYMBOLS[code.getString(0)] = code.getString(1);
}
} }
} } catch (e: IOException) {
} catch (e: IOException) { throw ModuleException(
throw ModuleException(
"loadCodes(): IOE", "loadCodes(): IOE",
"An IO error has occurred while retrieving the currencies.", "An IO error has occurred while retrieving the currencies.",
e e
) )
}
} }
} }
} }
init { init {
commands.add(CURRENCY_CMD) commands.add(CURRENCY_CMD)
loadSymbols() initProperties(API_KEY_PROP)
loadSymbols(properties[ChatGpt.API_KEY_PROP])
} }
} }

View file

@ -36,6 +36,7 @@ import assertk.assertions.contains
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 net.thauvin.erik.mobibot.LocalProperties
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.loadSymbols import net.thauvin.erik.mobibot.modules.CurrencyConverter.Companion.loadSymbols
import net.thauvin.erik.mobibot.msg.ErrorMessage import net.thauvin.erik.mobibot.msg.ErrorMessage
@ -48,32 +49,35 @@ import org.testng.annotations.Test
/** /**
* The `CurrencyConvertTest` class. * The `CurrencyConvertTest` class.
*/ */
class CurrencyConverterTest { class CurrencyConverterTest: LocalProperties() {
@BeforeClass @BeforeClass
@Throws(ModuleException::class) @Throws(ModuleException::class)
fun before() { fun before() {
loadSymbols() val apiKey = getProperty(CurrencyConverter.API_KEY_PROP)
loadSymbols(apiKey)
} }
@Test(groups = ["modules"]) @Test(groups = ["modules"])
fun testConvertCurrency() { fun testConvertCurrency() {
val apiKey = getProperty(CurrencyConverter.API_KEY_PROP)
assertThat( assertThat(
convertCurrency("100 USD to EUR").msg, convertCurrency(apiKey,"100 USD to EUR").msg,
"convertCurrency(100 USD to EUR)" "convertCurrency(100 USD to EUR)"
).matches("100 United States Dollar = \\d{2,3}\\.\\d+ Euro".toRegex()) ).matches("100 United States Dollar = \\d{2,3}\\.\\d+ Euro".toRegex())
assertThat( assertThat(
convertCurrency("1 USD to BTC").msg, convertCurrency(apiKey,"1 USD to GBP").msg,
"convertCurrency(1 USD to BTC)" "convertCurrency(1 USD to BGP)"
).matches("1 United States Dollar = 0\\.\\d+ Bitcoin".toRegex()) ).matches("1 United States Dollar = 0\\.\\d+ Pound Sterling".toRegex())
assertThat( assertThat(
convertCurrency("100,000.00 GBP to BTC").msg, convertCurrency(apiKey,"100,000.00 CAD to USD").msg,
"convertCurrency(100,000.00 GBP to BTC)" "convertCurrency(100,000.00 GBP to USD)"
).matches("100,000.00 British Pound Sterling = \\d{1,2}\\.\\d+ Bitcoin".toRegex()) ).matches("100,000.00 Canadian Dollar = \\d+\\.\\d+ United States Dollar".toRegex())
assertThat(convertCurrency("100 USD to USD"), "convertCurrency(100 USD to USD)").all { assertThat(convertCurrency(apiKey,"100 USD to USD"), "convertCurrency(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)
} }
assertThat(convertCurrency("100 USD"), "convertCurrency(100 USD)").all { assertThat(convertCurrency(apiKey,"100 USD"), "convertCurrency(100 USD)").all {
prop(Message::msg).contains("Invalid query.") prop(Message::msg).contains("Invalid query.")
isInstanceOf(ErrorMessage::class.java) isInstanceOf(ErrorMessage::class.java)
} }