Implemented CryptoPrices (Coinbase API), replacing the Bitcoin module.

This commit is contained in:
Erik C. Thauvin 2021-05-06 02:39:27 -07:00
parent 1f965a1833
commit f580f0f7f2
7 changed files with 145 additions and 168 deletions

View file

@ -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'

View file

@ -15,8 +15,8 @@
<ID>LongParameterList:Mobibot.kt$Mobibot$( nick: String, list: List&lt;String&gt;, maxPerLine: Int, isPrivate: Boolean, isBold: Boolean = false, isIndent: Boolean = false )</ID>
<ID>LongParameterList:Twitter.kt$Twitter.Companion$( consumerKey: String?, consumerSecret: String?, token: String?, tokenSecret: String?, handle: String?, message: String, isDm: Boolean )</ID>
<ID>NestedBlockDepth:Addons.kt$Addons$ fun add(command: AbstractCommand, props: Properties)</ID>
<ID>NestedBlockDepth:Bitcoin.kt$Bitcoin$ override fun run(sender: String, cmd: String, args: String, isPrivate: Boolean)</ID>
<ID>NestedBlockDepth:Comment.kt$Comment$override fun commandResponse( sender: String, login: String, args: String, isOp: Boolean, isPrivate: Boolean )</ID>
<ID>NestedBlockDepth:CryptoPrices.kt$CryptoPrices$ override fun run(sender: String, cmd: String, args: String, isPrivate: Boolean)</ID>
<ID>NestedBlockDepth:CurrencyConverter.kt$CurrencyConverter$ override fun run(sender: String, cmd: String, args: String, isPrivate: Boolean)</ID>
<ID>NestedBlockDepth:CurrencyConverter.kt$CurrencyConverter$override fun helpResponse(sender: String, isPrivate: Boolean): Boolean</ID>
<ID>NestedBlockDepth:CurrencyConverter.kt$CurrencyConverter.Companion$ @Suppress("MagicNumber") @JvmStatic fun convertCurrency(query: String): Message</ID>
@ -39,12 +39,12 @@
<ID>NestedBlockDepth:WorldTime.kt$WorldTime$override fun commandResponse( sender: String, cmd: String, args: String, isPrivate: Boolean )</ID>
<ID>ReturnCount:Addons.kt$Addons$ fun exec(sender: String, login: String, cmd: String, args: String, isOp: Boolean, isPrivate: Boolean): Boolean</ID>
<ID>ReturnCount:Addons.kt$Addons$ fun help(sender: String, topic: String, isOp: Boolean, isPrivate: Boolean): Boolean</ID>
<ID>ThrowsCount:Bitcoin.kt$Bitcoin.Companion$ @JvmStatic @Throws(ModuleException::class) fun marketPrice(currency: String): List&lt;Message&gt;</ID>
<ID>ThrowsCount:CryptoPrices.kt$CryptoPrices.Companion$ @JvmStatic @Throws(ModuleException::class) fun marketPrice(base: String, currency: String): Price</ID>
<ID>ThrowsCount:GoogleSearch.kt$GoogleSearch.Companion$ @JvmStatic @Throws(ModuleException::class) fun searchGoogle(query: String, apiKey: String?, cseKey: String?): List&lt;Message&gt;</ID>
<ID>ThrowsCount:StockQuote.kt$StockQuote.Companion$ @JvmStatic @Throws(ModuleException::class) fun getQuote(symbol: String, apiKey: String?): List&lt;Message&gt;</ID>
<ID>ThrowsCount:StockQuote.kt$StockQuote.Companion$@Throws(ModuleException::class) private fun getJsonResponse(response: String, debugMessage: String): JSONObject</ID>
<ID>ThrowsCount:Weather2.kt$Weather2.Companion$ @JvmStatic @Throws(ModuleException::class) fun getWeather(query: String, apiKey: String?): List&lt;Message&gt;</ID>
<ID>TooGenericExceptionCaught:Bitcoin.kt$Bitcoin.Companion$e: NullPointerException</ID>
<ID>TooGenericExceptionCaught:CryptoPrices.kt$CryptoPrices$e: Exception</ID>
<ID>TooGenericExceptionCaught:FeedReader.kt$FeedReader$e: Exception</ID>
<ID>TooGenericExceptionCaught:Mobibot.kt$Mobibot$e: Exception</ID>
<ID>TooGenericExceptionCaught:Mobibot.kt$Mobibot$ex: Exception</ID>

View file

@ -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)

View file

@ -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 <USD|GBP|EUR|...>",
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<Message> {
val debugMessage = "marketPrice($currency)"
val messages = mutableListOf<Message>()
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)
}
}

View file

@ -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 <symbol> [<currency>]"))
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"))
}
}

View file

@ -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)")
}
}

View file

@ -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