From f580f0f7f24fecea598ec93bace0d2b017047f94 Mon Sep 17 00:00:00 2001 From: "Erik C. Thauvin" Date: Thu, 6 May 2021 02:39:27 -0700 Subject: [PATCH] Implemented CryptoPrices (Coinbase API), replacing the Bitcoin module. --- build.gradle | 5 + config/detekt/baseline.xml | 6 +- .../net/thauvin/erik/mobibot/Mobibot.kt | 4 +- .../thauvin/erik/mobibot/modules/Bitcoin.kt | 149 ------------------ .../erik/mobibot/modules/CryptoPrices.kt | 103 ++++++++++++ .../{BitcoinTest.kt => CryptoPricesTest.kt} | 40 +++-- version.properties | 6 +- 7 files changed, 145 insertions(+), 168 deletions(-) delete mode 100644 src/main/kotlin/net/thauvin/erik/mobibot/modules/Bitcoin.kt create mode 100644 src/main/kotlin/net/thauvin/erik/mobibot/modules/CryptoPrices.kt rename src/test/kotlin/net/thauvin/erik/mobibot/modules/{BitcoinTest.kt => CryptoPricesTest.kt} (56%) 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