diff --git a/build.gradle b/build.gradle
index a1c957e..d55ba00 100644
--- a/build.gradle
+++ b/build.gradle
@@ -50,9 +50,14 @@ dependencies {
implementation 'org.apache.commons:commons-lang3:3.12.0'
implementation 'com.rometools:rome:1.15.0'
+ implementation 'com.squareup.okhttp3:okhttp:4.9.1'
implementation 'net.aksingh:owm-japis:2.5.3.0'
implementation 'net.objecthunter:exp4j:0.4.8'
+
+ implementation 'net.thauvin.erik:cryptoprice:0.9.0-SNAPSHOT'
implementation 'net.thauvin.erik:pinboard-poster:1.0.3'
+
+
implementation 'org.json:json:20210307'
implementation 'org.jsoup:jsoup:1.13.1'
implementation 'org.twitter4j:twitter4j-core:4.0.7'
diff --git a/config/detekt/baseline.xml b/config/detekt/baseline.xml
index a4065f6..5dab69a 100644
--- a/config/detekt/baseline.xml
+++ b/config/detekt/baseline.xml
@@ -15,8 +15,8 @@
LongParameterList:Mobibot.kt$Mobibot$( nick: String, list: List<String>, maxPerLine: Int, isPrivate: Boolean, isBold: Boolean = false, isIndent: Boolean = false )
LongParameterList:Twitter.kt$Twitter.Companion$( consumerKey: String?, consumerSecret: String?, token: String?, tokenSecret: String?, handle: String?, message: String, isDm: Boolean )
NestedBlockDepth:Addons.kt$Addons$ fun add(command: AbstractCommand, props: Properties)
- NestedBlockDepth:Bitcoin.kt$Bitcoin$ override fun run(sender: String, cmd: String, args: String, isPrivate: Boolean)
NestedBlockDepth:Comment.kt$Comment$override fun commandResponse( sender: String, login: String, args: String, isOp: Boolean, isPrivate: Boolean )
+ NestedBlockDepth:CryptoPrices.kt$CryptoPrices$ override fun run(sender: String, cmd: String, args: String, isPrivate: Boolean)
NestedBlockDepth:CurrencyConverter.kt$CurrencyConverter$ override fun run(sender: String, cmd: String, args: String, isPrivate: Boolean)
NestedBlockDepth:CurrencyConverter.kt$CurrencyConverter$override fun helpResponse(sender: String, isPrivate: Boolean): Boolean
NestedBlockDepth:CurrencyConverter.kt$CurrencyConverter.Companion$ @Suppress("MagicNumber") @JvmStatic fun convertCurrency(query: String): Message
@@ -39,12 +39,12 @@
NestedBlockDepth:WorldTime.kt$WorldTime$override fun commandResponse( sender: String, cmd: String, args: String, isPrivate: Boolean )
ReturnCount:Addons.kt$Addons$ fun exec(sender: String, login: String, cmd: String, args: String, isOp: Boolean, isPrivate: Boolean): Boolean
ReturnCount:Addons.kt$Addons$ fun help(sender: String, topic: String, isOp: Boolean, isPrivate: Boolean): Boolean
- ThrowsCount:Bitcoin.kt$Bitcoin.Companion$ @JvmStatic @Throws(ModuleException::class) fun marketPrice(currency: String): List<Message>
+ ThrowsCount:CryptoPrices.kt$CryptoPrices.Companion$ @JvmStatic @Throws(ModuleException::class) fun marketPrice(base: String, currency: String): Price
ThrowsCount:GoogleSearch.kt$GoogleSearch.Companion$ @JvmStatic @Throws(ModuleException::class) fun searchGoogle(query: String, apiKey: String?, cseKey: String?): List<Message>
ThrowsCount:StockQuote.kt$StockQuote.Companion$ @JvmStatic @Throws(ModuleException::class) fun getQuote(symbol: String, apiKey: String?): List<Message>
ThrowsCount:StockQuote.kt$StockQuote.Companion$@Throws(ModuleException::class) private fun getJsonResponse(response: String, debugMessage: String): JSONObject
ThrowsCount:Weather2.kt$Weather2.Companion$ @JvmStatic @Throws(ModuleException::class) fun getWeather(query: String, apiKey: String?): List<Message>
- TooGenericExceptionCaught:Bitcoin.kt$Bitcoin.Companion$e: NullPointerException
+ TooGenericExceptionCaught:CryptoPrices.kt$CryptoPrices$e: Exception
TooGenericExceptionCaught:FeedReader.kt$FeedReader$e: Exception
TooGenericExceptionCaught:Mobibot.kt$Mobibot$e: Exception
TooGenericExceptionCaught:Mobibot.kt$Mobibot$ex: Exception
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/Mobibot.kt b/src/main/kotlin/net/thauvin/erik/mobibot/Mobibot.kt
index 72f0e89..1efd54b 100644
--- a/src/main/kotlin/net/thauvin/erik/mobibot/Mobibot.kt
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/Mobibot.kt
@@ -64,7 +64,7 @@ import net.thauvin.erik.mobibot.commands.links.View
import net.thauvin.erik.mobibot.commands.tell.Tell
import net.thauvin.erik.mobibot.entries.EntriesMgr
import net.thauvin.erik.mobibot.entries.EntryLink
-import net.thauvin.erik.mobibot.modules.Bitcoin
+import net.thauvin.erik.mobibot.modules.CryptoPrices
import net.thauvin.erik.mobibot.modules.Calc
import net.thauvin.erik.mobibot.modules.CurrencyConverter
import net.thauvin.erik.mobibot.modules.Dice
@@ -678,8 +678,8 @@ class Mobibot(nickname: String, channel: String, logsDirPath: String, p: Propert
addons.add(View(this), p)
// Load the modules
- addons.add(Bitcoin(this), p)
addons.add(Calc(this), p)
+ addons.add(CryptoPrices(this), p)
addons.add(CurrencyConverter(this), p)
addons.add(Dice(this), p)
addons.add(GoogleSearch(this), p)
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/modules/Bitcoin.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/Bitcoin.kt
deleted file mode 100644
index db94db6..0000000
--- a/src/main/kotlin/net/thauvin/erik/mobibot/modules/Bitcoin.kt
+++ /dev/null
@@ -1,149 +0,0 @@
-/*
- * Bitcoin.kt
- *
- * Copyright (c) 2004-2021, Erik C. Thauvin (erik@thauvin.net)
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * Redistributions of source code must retain the above copyright notice, this
- * list of conditions and the following disclaimer.
- *
- * Redistributions in binary form must reproduce the above copyright notice,
- * this list of conditions and the following disclaimer in the documentation
- * and/or other materials provided with the distribution.
- *
- * Neither the name of this project nor the names of its contributors may be
- * used to endorse or promote products derived from this software without
- * specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
- * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
- * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
- * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
- * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
- * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package net.thauvin.erik.mobibot.modules
-
-import net.thauvin.erik.mobibot.Mobibot
-import net.thauvin.erik.mobibot.Utils
-import net.thauvin.erik.mobibot.msg.ErrorMessage
-import net.thauvin.erik.mobibot.msg.Message
-import net.thauvin.erik.mobibot.msg.NoticeMessage
-import net.thauvin.erik.mobibot.msg.PublicMessage
-import org.json.JSONException
-import org.json.JSONObject
-import java.io.IOException
-import java.net.URL
-import java.text.DecimalFormat
-
-/**
- * The Bitcoin module.
- */
-class Bitcoin(bot: Mobibot) : ThreadedModule(bot) {
- // Currencies
- private val currencies = listOf(
- "USD", "AUD", "BRL", "CAD", "CHF", "CLP", "CNY", "DKK", "EUR", "GBP", "HKD", "INR", "ISK", "JPY", "KRW",
- "NZD", "PLN", "RUB", "SEK", "SGD", "THB", "TRY", "TWD");
-
- override fun helpResponse(sender: String, isPrivate: Boolean): Boolean {
- with(bot) {
- send(sender, "To retrieve the bitcoin market price:", isPrivate)
- send(
- sender,
- Utils.helpFormat(
- Utils.buildCmdSyntax(
- "%c $BITCOIN_CMD ",
- nick,
- isPrivateMsgEnabled)
- ),
- isPrivate
- )
- send(sender, "The supported currencies are: ", isPrivate)
- @Suppress("MagicNumber")
- sendList(sender, currencies, 12, isPrivate, isIndent = true)
- }
- return true
- }
-
- /**
- * Returns the bitcoin market price from [Blockchain.info](https://blockchain.info/ticker).
- */
- override fun run(sender: String, cmd: String, args: String, isPrivate: Boolean) {
- with(bot) {
- val arg = args.trim().uppercase()
- @Suppress("MagicNumber")
- if (!currencies.contains(arg)) {
- helpResponse(sender, isPrivate)
- } else {
- try {
- val messages = marketPrice(arg)
- for (msg in messages) {
- send(sender, msg)
- }
- } catch (e: ModuleException) {
- if (logger.isWarnEnabled) logger.warn(e.debugMessage, e)
- send(e.message)
- }
- }
- }
- }
-
- companion object {
- // Blockchain Ticker URL
- private const val TICKER_URL = "https://blockchain.info/ticker"
-
- // Bitcoin command
- private const val BITCOIN_CMD = "bitcoin"
-
- // BTC command
- private const val BTC_CMD = "btc"
-
- private fun JSONObject.getDecimal(key: String): String {
- return DecimalFormat("0.00").format(this.getBigDecimal(key))
- }
-
- /**
- * Retrieves the bitcoin market price.
- */
- @JvmStatic
- @Throws(ModuleException::class)
- fun marketPrice(currency: String): List {
- val debugMessage = "marketPrice($currency)"
- val messages = mutableListOf()
- try {
- val response = Utils.urlReader(URL("$TICKER_URL"))
- val json = JSONObject(response)
- val bpi = json.getJSONObject(currency.trim().uppercase())
- val symbol = bpi.getString("symbol");
- with(messages) {
- add(PublicMessage("Bitcoin [BTC]: $symbol" + bpi.getDecimal("last") + " [$currency]"))
- if (bpi.getBigDecimal("15m") != bpi.getBigDecimal("last")) {
- add(NoticeMessage(" 15m: $symbol" + bpi.getDecimal("15m")))
- add(NoticeMessage(" Buy: $symbol" + bpi.getDecimal("buy")))
- add(NoticeMessage(" Sell: $symbol" + bpi.getDecimal("sell")))
- }
- }
- return messages
- } catch (e: IOException) {
- throw ModuleException(debugMessage, "An IO error has occurred retrieving the bitcoin market price.", e)
- } catch (e: NullPointerException) {
- throw ModuleException(debugMessage, "An error has occurred retrieving the bitcoin market price.", e)
- } catch (e: org.json.JSONException) {
- throw ModuleException(
- debugMessage, "A parsing error has occurred retriving the bitcoin market price.", e)
- }
- }
- }
-
- init {
- commands.add(BITCOIN_CMD)
- commands.add(BTC_CMD)
- }
-}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/modules/CryptoPrices.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/CryptoPrices.kt
new file mode 100644
index 0000000..20c42b8
--- /dev/null
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/CryptoPrices.kt
@@ -0,0 +1,103 @@
+/*
+ * CryptoPrices.kt
+ *
+ * Copyright (c) 2004-2021, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package net.thauvin.erik.mobibot.modules
+
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import net.thauvin.erik.crypto.CryptoPrice.Companion.marketPrice
+import net.thauvin.erik.crypto.CryptoException
+import net.thauvin.erik.mobibot.Mobibot
+import net.thauvin.erik.mobibot.Utils
+import net.thauvin.erik.mobibot.msg.ErrorMessage
+import net.thauvin.erik.mobibot.msg.Message
+import net.thauvin.erik.mobibot.msg.NoticeMessage
+import net.thauvin.erik.mobibot.msg.PublicMessage
+import org.json.JSONException
+import org.json.JSONObject
+import java.io.IOException
+import java.net.URL
+import java.text.DecimalFormat
+import java.time.format.DateTimeFormatter
+import java.time.Instant
+import java.time.temporal.ChronoUnit
+import java.time.ZoneId
+import java.time.ZoneOffset
+
+data class Price(val base: String, val currency: String, val amount: Double)
+
+/**
+ * The Cryptocurrency Prices module.
+ */
+class CryptoPrices(bot: Mobibot) : ThreadedModule(bot) {
+ val dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd").withZone(ZoneOffset.UTC)
+ val decimalFormat = DecimalFormat("0.00")
+
+ /**
+ * Returns the cryptocurrency market price from [Coinbase](https://developers.coinbase.com/api/v2#get-spot-price).
+ */
+ override fun run(sender: String, cmd: String, args: String, isPrivate: Boolean) {
+ val debugMessage = "crypto($cmd $args)"
+ with(bot) {
+ if (args.matches("\\w+( [a-zA-Z]{3}+)?".toRegex())) {
+ val params = args.trim().split(" ");
+ try {
+ val currency = if (params.size == 2) params[1] else "USD"
+ val price = marketPrice(params[0], currency)
+ send(sender, PublicMessage("${price.base}: ${price.amount} [${price.currency}]"))
+ } catch (e: CryptoException) {
+ if (logger.isWarnEnabled) logger.warn("$debugMessage => ${e.statusCode}", e)
+ send(e.message)
+ } catch (e: Exception) {
+ if (logger.isErrorEnabled) logger.error(debugMessage, e)
+ send("An error has occurred while retrieving the cryptocurrency market price.")
+ }
+ } else {
+ helpResponse(sender, isPrivate)
+ }
+ }
+ }
+
+ companion object {
+ // Crypto command
+ private const val CRYPTO_CMD = "crypto"
+ }
+
+ init {
+ commands.add(CRYPTO_CMD)
+ help.add("To retrieve a cryptocurrency's market price:")
+ help.add(Utils.helpFormat("%c $CRYPTO_CMD []"))
+ help.add("For example:")
+ help.add(Utils.helpFormat("%c $CRYPTO_CMD BTC"))
+ help.add(Utils.helpFormat("%c $CRYPTO_CMD ETH EUR"))
+ help.add(Utils.helpFormat("%c $CRYPTO_CMD ETH2 GPB"))
+ }
+}
diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/modules/BitcoinTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/modules/CryptoPricesTest.kt
similarity index 56%
rename from src/test/kotlin/net/thauvin/erik/mobibot/modules/BitcoinTest.kt
rename to src/test/kotlin/net/thauvin/erik/mobibot/modules/CryptoPricesTest.kt
index 2394bb5..bd07f1c 100644
--- a/src/test/kotlin/net/thauvin/erik/mobibot/modules/BitcoinTest.kt
+++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/CryptoPricesTest.kt
@@ -1,5 +1,5 @@
/*
- * BitcoinTest.kt
+ * CryptoPricesTest.kt
*
* Copyright (c) 2004-2021, Erik C. Thauvin (erik@thauvin.net)
* All rights reserved.
@@ -31,25 +31,43 @@
*/
package net.thauvin.erik.mobibot.modules
+import net.thauvin.erik.crypto.CryptoPrice.Companion.marketPrice
+import net.thauvin.erik.crypto.CryptoException
import net.thauvin.erik.mobibot.LocalProperties
-import net.thauvin.erik.mobibot.modules.Bitcoin.Companion.marketPrice
import org.assertj.core.api.Assertions.assertThat
+import org.assertj.core.api.Assertions.assertThatThrownBy
import org.testng.annotations.Test
/**
- * The `BitcoinTest` class.
+ * The `CryptoPricesTest` class.
*/
-class BitcoinTest : LocalProperties() {
+class CryptoPricesTest {
@Test
@Throws(ModuleException::class)
fun testMarketPrice() {
- var messages = marketPrice("USD")
- assertThat(messages).`as`("not empty").isNotEmpty
- assertThat(messages[0].msg).`as`("bitcoin, BTC, $").startsWith("Bitcoin").contains("BTC").contains("$")
- //assertThat(messages[1].msg).`as`("15m").contains("15m")
+ var price = marketPrice("BTC", "USD")
+ assertThat(price.base).`as`("is BTC").isEqualTo("BTC")
+ assertThat(price.currency).`as`("is USD").isEqualTo("USD")
+ assertThat(price.amount).`as`("BTC > 0").isGreaterThan(0.00)
- messages = marketPrice("GBP")
- assertThat(messages[0].msg).`as`("£").contains("£").contains("GBP")
- //assertThat(messages[1].msg).`as`("GBP 15m").contains("15m")
+ price = marketPrice("ETH", "EUR")
+ assertThat(price.base).`as`("is ETH").isEqualTo("ETH")
+ assertThat(price.currency).`as`("is EUR").isEqualTo("EUR")
+ assertThat(price.amount).`as`("ETH > 0").isGreaterThan(0.00)
+
+ price = marketPrice("ETH2", "GBP")
+ assertThat(price.base).`as`("is ETH2").isEqualTo("ETH2")
+ assertThat(price.currency).`as`("is GBP").isEqualTo("GBP")
+ assertThat(price.amount).`as`("ETH2 > 0").isGreaterThan(0.00)
+
+ assertThatThrownBy { marketPrice("FOO", "USD") }
+ .`as`("FOO")
+ .isInstanceOf(CryptoException::class.java)
+ .hasMessageContaining("Invalid base currency")
+
+ assertThatThrownBy { marketPrice("FOO", "BAR") }
+ .`as`("FOO-BAR")
+ .isInstanceOf(CryptoException::class.java)
+ .hasMessageContaining("Invalid currency (BAR)")
}
}
diff --git a/version.properties b/version.properties
index 80c6644..f827da8 100644
--- a/version.properties
+++ b/version.properties
@@ -1,9 +1,9 @@
#Generated by the Semver Plugin for Gradle
-#Tue May 04 23:36:08 PDT 2021
-version.buildmeta=632
+#Sat May 08 02:52:53 PDT 2021
+version.buildmeta=688
version.major=0
version.minor=8
version.patch=0
version.prerelease=beta
version.project=mobibot
-version.semver=0.8.0-beta+632
+version.semver=0.8.0-beta+688