diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
index 0273acf..9156431 100644
--- a/.idea/kotlinc.xml
+++ b/.idea/kotlinc.xml
@@ -7,10 +7,7 @@
-
-
-
-
-
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
index bccad37..fe0e283 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,14 +1,124 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
\ No newline at end of file
diff --git a/.idea/runConfigurations/Run Tests.xml b/.idea/runConfigurations/Run Tests.xml
deleted file mode 100644
index 37dc742..0000000
--- a/.idea/runConfigurations/Run Tests.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/config/detekt/baseline.xml b/config/detekt/baseline.xml
index 719ca4e..502be67 100644
--- a/config/detekt/baseline.xml
+++ b/config/detekt/baseline.xml
@@ -1,6 +1,6 @@
-
+
CyclomaticComplexMethod:FeedsManager.kt$FeedsManager.Companion$@JvmStatic fun saveFeed(entries: Entries, currentFile: String = CURRENT_XML)
CyclomaticComplexMethod:Weather2.kt$Weather2.Companion$@JvmStatic @Throws(ModuleException::class) fun getWeather(query: String, apiKey: String?): List<Message>
@@ -10,8 +10,6 @@
LongMethod:Weather2.kt$Weather2.Companion$@JvmStatic @Throws(ModuleException::class) fun getWeather(query: String, apiKey: String?): List<Message>
LongParameterList:Comment.kt$Comment$( channel: String, cmd: String, entry: EntryLink, entryIndex: Int, commentIndex: Int, event: GenericMessageEvent )
LongParameterList:EntryLink.kt$EntryLink$( // Link's comments val comments: MutableList<EntryComment> = mutableListOf(), // Tags/categories val tags: MutableList<SyndCategory> = mutableListOf(), // Channel var channel: String, // Creation date var date: Date = Calendar.getInstance().time, // Link's URL var link: String, // Author's login var login: String = "", // Author's nickname var nick: String, // Link's title var title: String )
- MagicNumber:ChatGpt.kt$ChatGpt.Companion$200
- MagicNumber:ChatGpt.kt$ChatGpt.Companion$429
MagicNumber:Comment.kt$Comment$3
MagicNumber:CryptoPrices.kt$CryptoPrices$10
MagicNumber:CurrencyConverter.kt$CurrencyConverter$11
@@ -47,7 +45,6 @@
MagicNumber:WorldTime.kt$WorldTime.Companion$86.4
NestedBlockDepth:Addons.kt$Addons$fun add(command: AbstractCommand): Boolean
NestedBlockDepth:Addons.kt$Addons$fun add(module: AbstractModule): Boolean
- NestedBlockDepth:ChatGpt.kt$ChatGpt.Companion$@JvmStatic @Throws(ModuleException::class) fun chat(query: String, apiKey: String?, maxTokens: Int): String
NestedBlockDepth:Comment.kt$Comment$override fun commandResponse(channel: String, args: String, event: GenericMessageEvent)
NestedBlockDepth:CurrencyConverter.kt$CurrencyConverter.Companion$@JvmStatic @Throws(ModuleException::class) fun loadSymbols(apiKey: String?)
NestedBlockDepth:CurrencyConverter.kt$CurrencyConverter.Companion$@JvmStatic fun convertCurrency(apiKey: String?, query: String): Message
@@ -71,7 +68,6 @@
ReturnCount:Addons.kt$Addons$fun help(channel: String, topic: String, event: GenericMessageEvent): Boolean
ReturnCount:ExceptionSanitizer.kt$ExceptionSanitizer$fun ModuleException.sanitize(vararg sanitize: String): ModuleException
ReturnCount:Seen.kt$Seen$override fun commandResponse(channel: String, args: String, event: GenericMessageEvent)
- ThrowsCount:ChatGpt.kt$ChatGpt.Companion$@JvmStatic @Throws(ModuleException::class) fun chat(query: String, apiKey: String?, maxTokens: Int): String
ThrowsCount:GoogleSearch.kt$GoogleSearch.Companion$@JvmStatic @Throws(ModuleException::class) fun searchGoogle( query: String, apiKey: String?, cseKey: String?, quotaUser: String = ReleaseInfo.PROJECT ): List<Message>
ThrowsCount:Joke.kt$Joke.Companion$@JvmStatic @Throws(ModuleException::class) fun randomJoke(): List<Message>
ThrowsCount:Mastodon.kt$Mastodon.Companion$@JvmStatic @Throws(ModuleException::class) fun toot(apiKey: String?, instance: String?, handle: String?, message: String, isDm: Boolean): String
@@ -80,7 +76,6 @@
ThrowsCount:Weather2.kt$Weather2.Companion$@JvmStatic @Throws(ModuleException::class) fun getWeather(query: String, apiKey: String?): List<Message>
ThrowsCount:WolframAlpha.kt$WolframAlpha.Companion$@JvmStatic @Throws(ModuleException::class) fun queryWolfram(query: String, units: String = IMPERIAL, appId: String?): String
TooGenericExceptionCaught:ChatGpt2.kt$ChatGpt2.Companion$e: Exception
- TooGenericExceptionCaught:Gemini.kt$Gemini.Companion$e: Exception
TooGenericExceptionCaught:Gemini2.kt$Gemini2.Companion$e: Exception
TooGenericExceptionCaught:StockQuote.kt$StockQuote.Companion$e: NullPointerException
TooGenericExceptionCaught:Weather2.kt$Weather2.Companion$e: NullPointerException
@@ -89,14 +84,16 @@
TooManyFunctions:Tell.kt$Tell : AbstractCommand
UtilityClassWithPublicConstructor:LocalProperties.kt$LocalProperties
WildcardImport:AddonsTest.kt$import net.thauvin.erik.mobibot.modules.*
+ WildcardImport:CryptoPricesTest.kt$import assertk.assertions.*
+ WildcardImport:CurrencyConverterTest.kt$import assertk.assertions.*
WildcardImport:EntryLinkTest.kt$import assertk.assertions.*
WildcardImport:FeedMgrTest.kt$import assertk.assertions.*
WildcardImport:FeedReaderTest.kt$import assertk.assertions.*
WildcardImport:FeedsManager.kt$import com.rometools.rome.feed.synd.*
WildcardImport:Gemini2Test.kt$import assertk.assertions.*
- WildcardImport:GeminiTest.kt$import assertk.assertions.*
WildcardImport:GoogleSearchTest.kt$import assertk.assertions.*
WildcardImport:JokeTest.kt$import assertk.assertions.*
+ WildcardImport:LinksManagerTest.kt$import assertk.assertions.*
WildcardImport:Mobibot.kt$import java.io.*
WildcardImport:Mobibot.kt$import net.thauvin.erik.mobibot.commands.*
WildcardImport:Mobibot.kt$import net.thauvin.erik.mobibot.commands.links.*
diff --git a/pom.xml b/pom.xml
index 58768ef..7d916f4 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
4.0.0
net.thauvin.erik.mobibot
mobibot
- 0.8.0-rc+20250424113056
+ 0.8.0-rc+20250509073816
mobibot
@@ -168,7 +168,7 @@
org.jsoup
jsoup
- 1.19.1
+ 1.20.1
compile
diff --git a/properties/mobibot.properties b/properties/mobibot.properties
index fd7b104..0520fdd 100644
--- a/properties/mobibot.properties
+++ b/properties/mobibot.properties
@@ -16,6 +16,8 @@ ident=changeme
logs=./logs
ignore=chanserv,nickserv
tags=mobile mobitopia
+
+# Keywords to add as tags if found in links titles
tags-keywords=android ios apple google
feed=http://www.mobitopia.org/rss.xml
@@ -68,7 +70,7 @@ disabled-modules=mastodon
#alphavantage-api-key=
#
-# Get Wolfram Alpha AppID from: https://developer.wolframalpha.com/portal/
+# Get Wolfram Alpha AppID from: https://developer.wolframalpha.com/
#
#wolfram-appid=
#wolfram-units=imperial
diff --git a/src/bld/java/net/thauvin/erik/MobibotBuild.java b/src/bld/java/net/thauvin/erik/MobibotBuild.java
index 08367ef..dde5dc8 100644
--- a/src/bld/java/net/thauvin/erik/MobibotBuild.java
+++ b/src/bld/java/net/thauvin/erik/MobibotBuild.java
@@ -38,7 +38,6 @@ import rife.bld.extension.CompileKotlinOperation;
import rife.bld.extension.DetektOperation;
import rife.bld.extension.GeneratedVersionOperation;
import rife.bld.extension.JacocoReportOperation;
-import rife.bld.extension.kotlin.CompileOptions;
import rife.bld.operations.exceptions.ExitStatusException;
import rife.bld.publish.PomBuilder;
import rife.tools.FileUtils;
@@ -72,8 +71,10 @@ public class MobibotBuild extends Project {
mainClass = pkg + ".Mobibot";
javaRelease = 17;
- downloadSources = true;
+
autoDownloadPurge = true;
+ downloadSources = true;
+
repositories = List.of(
MAVEN_LOCAL,
MAVEN_CENTRAL,
@@ -117,14 +118,19 @@ public class MobibotBuild extends Project {
.include(dependency("net.aksingh", "owm-japis", "2.5.3.0"))
.include(dependency("net.objecthunter", "exp4j", "0.4.8"))
.include(dependency("org.json", "json", "20250107"))
- .include(dependency("org.jsoup", "jsoup", "1.19.1"))
+ .include(dependency("org.jsoup", "jsoup", "1.20.1"))
// Thauvin
.include(dependency("net.thauvin.erik", "cryptoprice", "1.0.3-SNAPSHOT"))
.include(dependency("net.thauvin.erik", "jokeapi", "1.0.1-SNAPSHOT"))
.include(dependency("net.thauvin.erik", "pinboard-poster", "1.2.1-SNAPSHOT"))
.include(dependency("net.thauvin.erik.urlencoder", "urlencoder-lib-jvm", "1.6.0"));
scope(test)
+ // Mockito
+ .include(dependency("net.bytebuddy", "byte-buddy", version(1, 17, 5)))
+ .include(dependency("org.mockito.kotlin", "mockito-kotlin", version(5, 4, 0)))
+ // AssertK
.include(dependency("com.willowtreeapps.assertk", "assertk-jvm", version(0, 28, 1)))
+ // JUnit
.include(dependency("org.jetbrains.kotlin", "kotlin-test-junit5", kotlin))
.include(dependency("org.junit.jupiter", "junit-jupiter", version(5, 12, 2)))
.include(dependency("org.junit.platform", "junit-platform-console-standalone", version(1, 12, 2)))
@@ -166,10 +172,9 @@ public class MobibotBuild extends Project {
@Override
public void compile() throws Exception {
releaseInfo();
- new CompileKotlinOperation()
- .compileOptions(new CompileOptions().progressive(true).verbose(true))
- .fromProject(this)
- .execute();
+ var op = new CompileKotlinOperation().fromProject(this);
+ op.compileOptions().languageVersion("2.1").progressive(true).verbose(true);
+ op.execute();
}
@Override
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/Addons.kt b/src/main/kotlin/net/thauvin/erik/mobibot/Addons.kt
index ed34897..9063e3e 100644
--- a/src/main/kotlin/net/thauvin/erik/mobibot/Addons.kt
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/Addons.kt
@@ -41,7 +41,7 @@ import org.slf4j.LoggerFactory
import java.util.*
/**
- * Modules and Commands addons.
+ * Registers and manages commands and modules.
*/
class Addons(private val props: Properties) {
private val logger: Logger = LoggerFactory.getLogger(Addons::class.java)
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/Constants.kt b/src/main/kotlin/net/thauvin/erik/mobibot/Constants.kt
index 0dea8d5..6ee3e08 100644
--- a/src/main/kotlin/net/thauvin/erik/mobibot/Constants.kt
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/Constants.kt
@@ -30,10 +30,16 @@
*/
package net.thauvin.erik.mobibot
+
/**
- * The `Constants`.
+ * The global constants.
*/
object Constants {
+ /**
+ * CLI command for usage.
+ */
+ const val CLI_CMD = "java -jar ${ReleaseInfo.PROJECT}.jar"
+
/**
* The connect/read timeout in ms.
*/
@@ -54,16 +60,6 @@ object Constants {
*/
const val DEFAULT_SERVER = "irc.libera.chat"
- /**
- * CLI command for usage.
- */
- const val CLI_CMD = "java -jar ${ReleaseInfo.PROJECT}.jar"
-
- /**
- * User-Agent
- */
- const val USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64; rv:136.0) Gecko/20100101 Firefox/136.0"
-
/**
* The help command.
*/
@@ -80,7 +76,7 @@ object Constants {
const val NO_TITLE = "No Title"
/**
- * Properties command line argument.
+ * `Properties` command line argument.
*/
const val PROPS_ARG = "properties"
@@ -94,6 +90,11 @@ object Constants {
*/
const val TIMER_DELAY = 10L
+ /**
+ * User-Agent
+ */
+ const val USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64; rv:136.0) Gecko/20100101 Firefox/136.0"
+
/**
* Properties version line argument.
*/
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/FeedReader.kt b/src/main/kotlin/net/thauvin/erik/mobibot/FeedReader.kt
index 3ab8d1f..2ab6fae 100644
--- a/src/main/kotlin/net/thauvin/erik/mobibot/FeedReader.kt
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/FeedReader.kt
@@ -51,23 +51,6 @@ import java.net.URL
class FeedReader(private val url: String, val event: GenericMessageEvent) : Runnable {
private val logger: Logger = LoggerFactory.getLogger(FeedsManager::class.java)
- /**
- * Fetches the Feed's items.
- */
- override fun run() {
- try {
- readFeed(url).forEach {
- event.sendMessage("", it)
- }
- } catch (e: FeedException) {
- if (logger.isWarnEnabled) logger.warn("Unable to parse the feed at $url", e)
- event.sendMessage("An error has occurred while parsing the feed: ${e.message}")
- } catch (e: IOException) {
- if (logger.isWarnEnabled) logger.warn("Unable to fetch the feed at $url", e)
- event.sendMessage("An IO error has occurred while fetching the feed: ${e.message}")
- }
- }
-
companion object {
@JvmStatic
@Throws(FeedException::class, IOException::class)
@@ -89,4 +72,21 @@ class FeedReader(private val url: String, val event: GenericMessageEvent) : Runn
return messages
}
}
+
+ /**
+ * Fetches the Feed's items.
+ */
+ override fun run() {
+ try {
+ readFeed(url).forEach {
+ event.sendMessage("", it)
+ }
+ } catch (e: FeedException) {
+ if (logger.isWarnEnabled) logger.warn("Unable to parse the feed at $url", e)
+ event.sendMessage("An error has occurred while parsing the feed: ${e.message}")
+ } catch (e: IOException) {
+ if (logger.isWarnEnabled) logger.warn("Unable to fetch the feed at $url", e)
+ event.sendMessage("An IO error has occurred while fetching the feed: ${e.message}")
+ }
+ }
}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/Mobibot.kt b/src/main/kotlin/net/thauvin/erik/mobibot/Mobibot.kt
index 691a339..ee726fc 100644
--- a/src/main/kotlin/net/thauvin/erik/mobibot/Mobibot.kt
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/Mobibot.kt
@@ -36,7 +36,7 @@ import kotlinx.cli.ArgType
import kotlinx.cli.default
import net.thauvin.erik.mobibot.Utils.appendIfMissing
import net.thauvin.erik.mobibot.Utils.bot
-import net.thauvin.erik.mobibot.Utils.capitalise
+import net.thauvin.erik.mobibot.Utils.capitalize
import net.thauvin.erik.mobibot.Utils.getIntProperty
import net.thauvin.erik.mobibot.Utils.helpCmdSyntax
import net.thauvin.erik.mobibot.Utils.helpFormat
@@ -81,148 +81,6 @@ class Mobibot(nickname: String, val channel: String, logsDirPath: String, p: Pro
/** Logger. */
val logger: Logger = LoggerFactory.getLogger(Mobibot::class.java)
- /**
- * Connects to the server and joins the channel.
- */
- fun connect() {
- PircBotX(config).startBot()
- }
-
- /**
- * Responds with the default help.
- */
- private fun helpDefault(event: GenericMessageEvent) {
- event.sendMessage("Type a URL on $channel to post it.")
- event.sendMessage("For more information on a specific command, type:")
- event.sendMessage(
- helpFormat(
- helpCmdSyntax("%c ${Constants.HELP_CMD} ", event.bot().nick, event is PrivateMessageEvent)
- )
- )
- event.sendMessage("The commands are:")
- event.sendList(addons.names.commands, 8, isBold = true, isIndent = true)
- if (event.isChannelOp(channel)) {
- if (addons.names.disabledCommands.isNotEmpty()) {
- event.sendMessage("The disabled commands are:")
- event.sendList(addons.names.disabledCommands, 8, isBold = false, isIndent = true)
- }
- event.sendMessage("The op commands are:")
- event.sendList(addons.names.ops, 8, isBold = true, isIndent = true)
- }
- }
-
- /**
- * Responds with the default, commands or modules help.
- */
- private fun helpResponse(event: GenericMessageEvent, topic: String) {
- if (topic.isBlank() || !addons.help(channel, topic.lowercase().trim(), event)) {
- helpDefault(event)
- }
- }
-
- override fun onAction(event: ActionEvent?) {
- event?.channel?.let {
- if (channel == it.name) {
- event.user?.let { user ->
- storeRecap(user.nick, event.action, true)
- }
- }
- }
- }
-
- override fun onDisconnect(event: DisconnectEvent?) {
- event?.let {
- with(event.getBot()) {
- LinksManager.socialManager.notification("$nick disconnected from $serverHostname")
- seen.add(userChannelDao.getChannel(channel).users)
- }
- }
- LinksManager.socialManager.shutdown()
- }
-
- override fun onPrivateMessage(event: PrivateMessageEvent?) {
- event?.user?.let { user ->
- if (logger.isTraceEnabled) logger.trace("<<< ${user.nick}: ${event.message}")
- val cmds = event.message.trim().split(" ".toRegex(), 2)
- val cmd = cmds[0].lowercase()
- val args = cmds.lastOrEmpty().trim()
- if (cmd.startsWith(Constants.HELP_CMD)) { // help
- helpResponse(event, args)
- } else if (!addons.exec(channel, cmd, args, event)) { // Execute command or module
- helpDefault(event)
- }
- }
- }
-
- override fun onJoin(event: JoinEvent?) {
- event?.user?.let { user ->
- with(event.getBot()) {
- if (user.nick == nick) {
- LinksManager.socialManager.notification(
- "$nick has joined ${event.channel.name} on $serverHostname"
- )
- seen.add(userChannelDao.getChannel(channel).users)
- } else {
- tell.send(event)
- seen.add(user.nick)
- }
- }
- }
- }
-
- override fun onMessage(event: MessageEvent?) {
- event?.user?.let { user ->
- tell.send(event)
- if (event.message.matches("(?i)${Pattern.quote(event.bot().nick)}:.*".toRegex())) { // mobibot:
- if (logger.isTraceEnabled) logger.trace(">>> ${user.nick}: ${event.message}")
- val cmds = event.message.substring(event.bot().nick.length + 1).trim().split(" ".toRegex(), 2)
- val cmd = cmds[0].lowercase()
- val args = cmds.lastOrEmpty().trim()
- if (cmd.startsWith(Constants.HELP_CMD)) { // mobibot: help
- helpResponse(event, args)
- } else {
- // Execute module or command
- addons.exec(channel, cmd, args, event)
- }
- } else if (addons.match(channel, event)) { // Links, e.g.: https://www.example.com/ or L1: , etc.
- if (logger.isTraceEnabled) logger.trace(">>> ${user.nick}: ${event.message}")
- }
- storeRecap(user.nick, event.message, false)
- seen.add(user.nick)
- }
- }
-
- override fun onNickChange(event: NickChangeEvent?) {
- event?.let {
- tell.send(event)
- if (!it.oldNick.equals(it.newNick, true)) {
- seen.add(it.oldNick)
- }
- seen.add(it.newNick)
- }
- }
-
- override fun onPart(event: PartEvent?) {
- event?.user?.let { user ->
- with(event.getBot()) {
- if (user.nick == nick) {
- LinksManager.socialManager.notification(
- "$nick has left ${event.channel.name} on $serverHostname"
- )
- seen.add(userChannelDao.getChannel(channel).users)
- } else {
- seen.add(user.nick)
- }
- }
- }
- }
-
- override fun onQuit(event: QuitEvent?) {
- event?.user?.let { user ->
- seen.add(user.nick)
- }
- }
-
companion object {
@JvmStatic
@Throws(Exception::class)
@@ -254,7 +112,7 @@ class Mobibot(nickname: String, val channel: String, logsDirPath: String, p: Pro
if (version) {
// Output the version
println(
- "${ReleaseInfo.PROJECT.capitalise()} ${ReleaseInfo.VERSION}" +
+ "${ReleaseInfo.PROJECT.capitalize()} ${ReleaseInfo.VERSION}" +
" (${ReleaseInfo.BUILD_DATE.toIsoLocalDate()})"
)
println(ReleaseInfo.WEBSITE)
@@ -267,10 +125,10 @@ class Mobibot(nickname: String, val channel: String, logsDirPath: String, p: Pro
).use { fis ->
p.load(fis)
}
- } catch (ignore: FileNotFoundException) {
+ } catch (_: FileNotFoundException) {
System.err.println("Unable to find properties file.")
exitProcess(1)
- } catch (ignore: IOException) {
+ } catch (_: IOException) {
System.err.println("Unable to open properties file.")
exitProcess(1)
}
@@ -289,7 +147,7 @@ class Mobibot(nickname: String, val channel: String, logsDirPath: String, p: Pro
), true
)
System.setOut(stdout)
- } catch (ignore: IOException) {
+ } catch (_: IOException) {
System.err.println("Unable to open output (stdout) log file.")
exitProcess(1)
}
@@ -300,7 +158,7 @@ class Mobibot(nickname: String, val channel: String, logsDirPath: String, p: Pro
), true
)
System.setErr(stderr)
- } catch (ignore: IOException) {
+ } catch (_: IOException) {
System.err.println("Unable to open error (stderr) log file.")
exitProcess(1)
}
@@ -416,5 +274,145 @@ class Mobibot(nickname: String, val channel: String, logsDirPath: String, p: Pro
// Sort the addons
addons.names.sort()
}
+
+ /**
+ * Connects to the server and joins the channel.
+ */
+ fun connect() {
+ PircBotX(config).startBot()
+ }
+
+ /**
+ * Responds with the default help.
+ */
+ private fun helpDefault(event: GenericMessageEvent) {
+ event.sendMessage("Type a URL on $channel to post it.")
+ event.sendMessage("For more information on a specific command, type:")
+ event.sendMessage(
+ helpFormat(
+ helpCmdSyntax("%c ${Constants.HELP_CMD} ", event.bot().nick, event is PrivateMessageEvent)
+ )
+ )
+ event.sendMessage("The commands are:")
+ event.sendList(addons.names.commands, 8, isBold = true, isIndent = true)
+ if (event.isChannelOp(channel)) {
+ if (addons.names.disabledCommands.isNotEmpty()) {
+ event.sendMessage("The disabled commands are:")
+ event.sendList(addons.names.disabledCommands, 8, isBold = false, isIndent = true)
+ }
+ event.sendMessage("The op commands are:")
+ event.sendList(addons.names.ops, 8, isBold = true, isIndent = true)
+ }
+ }
+
+ /**
+ * Responds with the default, commands or modules help.
+ */
+ private fun helpResponse(event: GenericMessageEvent, topic: String) {
+ if (topic.isBlank() || !addons.help(channel, topic.lowercase().trim(), event)) {
+ helpDefault(event)
+ }
+ }
+
+ override fun onAction(event: ActionEvent?) {
+ event?.channel?.let {
+ if (channel == it.name) {
+ event.user?.let { user ->
+ storeRecap(user.nick, event.action, true)
+ }
+ }
+ }
+ }
+
+ override fun onDisconnect(event: DisconnectEvent?) {
+ event?.let {
+ with(event.getBot()) {
+ LinksManager.socialManager.notification("$nick disconnected from $serverHostname")
+ seen.add(userChannelDao.getChannel(channel).users)
+ }
+ }
+ LinksManager.socialManager.shutdown()
+ }
+
+ override fun onPrivateMessage(event: PrivateMessageEvent?) {
+ event?.user?.let { user ->
+ if (logger.isTraceEnabled) logger.trace("<<< ${user.nick}: ${event.message}")
+ val cmds = event.message.trim().split(" ".toRegex(), 2)
+ val cmd = cmds[0].lowercase()
+ val args = cmds.lastOrEmpty().trim()
+ if (cmd.startsWith(Constants.HELP_CMD)) { // help
+ helpResponse(event, args)
+ } else if (!addons.exec(channel, cmd, args, event)) { // Execute command or module
+ helpDefault(event)
+ }
+ }
+ }
+
+ override fun onJoin(event: JoinEvent?) {
+ event?.user?.let { user ->
+ with(event.getBot()) {
+ if (user.nick == nick) {
+ LinksManager.socialManager.notification(
+ "$nick has joined ${event.channel.name} on $serverHostname"
+ )
+ seen.add(userChannelDao.getChannel(channel).users)
+ } else {
+ tell.send(event)
+ seen.add(user.nick)
+ }
+ }
+ }
+ }
+
+ override fun onMessage(event: MessageEvent?) {
+ event?.user?.let { user ->
+ if (logger.isTraceEnabled) logger.trace(">>> ${user.nick}: ${event.message}")
+ tell.send(event)
+ if (event.message.matches("(?i)${Pattern.quote(event.bot().nick)}:.*".toRegex())) { // mobibot:
+ val cmds = event.message.substring(event.bot().nick.length + 1).trim().split(" ".toRegex(), 2)
+ val cmd = cmds[0].lowercase()
+ val args = cmds.lastOrEmpty().trim()
+ if (cmd.startsWith(Constants.HELP_CMD)) { // mobibot: help
+ helpResponse(event, args)
+ } else {
+ // Execute module or command
+ addons.exec(channel, cmd, args, event)
+ }
+ }
+ storeRecap(user.nick, event.message, false)
+ seen.add(user.nick)
+ }
+ }
+
+ override fun onNickChange(event: NickChangeEvent?) {
+ event?.let {
+ tell.send(event)
+ if (!it.oldNick.equals(it.newNick, true)) {
+ seen.add(it.oldNick)
+ }
+ seen.add(it.newNick)
+ }
+ }
+
+ override fun onPart(event: PartEvent?) {
+ event?.user?.let { user ->
+ with(event.getBot()) {
+ if (user.nick == nick) {
+ LinksManager.socialManager.notification(
+ "$nick has left ${event.channel.name} on $serverHostname"
+ )
+ seen.add(userChannelDao.getChannel(channel).users)
+ } else {
+ seen.add(user.nick)
+ }
+ }
+ }
+ }
+
+ override fun onQuit(event: QuitEvent?) {
+ event?.user?.let { user ->
+ seen.add(user.nick)
+ }
+ }
}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/Pinboard.kt b/src/main/kotlin/net/thauvin/erik/mobibot/Pinboard.kt
index f9076c9..c927b8a 100644
--- a/src/main/kotlin/net/thauvin/erik/mobibot/Pinboard.kt
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/Pinboard.kt
@@ -40,7 +40,7 @@ import java.time.temporal.ChronoUnit
import java.util.*
/**
- * Handles posts to pinboard.in.
+ * Handles posts to `pinboard.in`.
*/
class Pinboard {
private val poster = PinboardPoster()
@@ -56,13 +56,6 @@ class Pinboard {
}
}
- /**
- * Sets the pinboard API token.
- */
- fun setApiToken(apiToken: String) {
- poster.apiToken = apiToken
- }
-
/**
* Deletes a pin.
*/
@@ -73,6 +66,13 @@ class Pinboard {
}
+ /**
+ * Sets the pinboard API token.
+ */
+ fun setApiToken(apiToken: String) {
+ poster.apiToken = apiToken
+ }
+
/**
* Updates a pin.
*/
@@ -87,15 +87,6 @@ class Pinboard {
}
}
- /**
- * Formats a date to a UTC timestamp.
- */
- private fun Date.toTimestamp(): String {
- return ZonedDateTime.ofInstant(
- toInstant().truncatedTo(ChronoUnit.SECONDS), ZoneId.systemDefault()
- ).format(DateTimeFormatter.ISO_INSTANT)
- }
-
/**
* Formats the tags for pinboard.
*/
@@ -109,5 +100,14 @@ class Pinboard {
private fun EntryLink.postedBy(ircServer: String): String {
return "Posted by $nick on $channel ( $ircServer )"
}
+
+ /**
+ * Formats a date to a UTC timestamp.
+ */
+ private fun Date.toTimestamp(): String {
+ return ZonedDateTime.ofInstant(
+ toInstant().truncatedTo(ChronoUnit.SECONDS), ZoneId.systemDefault()
+ ).format(DateTimeFormatter.ISO_INSTANT)
+ }
}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/ReleaseInfo.kt b/src/main/kotlin/net/thauvin/erik/mobibot/ReleaseInfo.kt
index 42a61aa..7b347fb 100644
--- a/src/main/kotlin/net/thauvin/erik/mobibot/ReleaseInfo.kt
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/ReleaseInfo.kt
@@ -14,12 +14,12 @@ import java.time.ZoneId
*/
object ReleaseInfo {
const val PROJECT = "mobibot"
- const val VERSION = "0.8.0-rc+20250322004101"
+ const val VERSION = "0.8.0-rc+20250509075545"
@JvmField
@Suppress("MagicNumber")
val BUILD_DATE: LocalDateTime = LocalDateTime.ofInstant(
- Instant.ofEpochMilli(1742629261438L), ZoneId.systemDefault()
+ Instant.ofEpochMilli(1746802545281L), ZoneId.systemDefault()
)
const val WEBSITE = "https://mobitopia.org/mobibot/"
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/Utils.kt b/src/main/kotlin/net/thauvin/erik/mobibot/Utils.kt
index 97797ef..d5d9b30 100644
--- a/src/main/kotlin/net/thauvin/erik/mobibot/Utils.kt
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/Utils.kt
@@ -52,7 +52,7 @@ import kotlin.io.path.exists
import kotlin.io.path.fileSize
/**
- * Miscellaneous utilities.
+ * Miscellaneous utility functions.
*/
@Suppress("TooManyFunctions")
object Utils {
@@ -111,13 +111,13 @@ object Utils {
* Capitalize a string.
*/
@JvmStatic
- fun String.capitalise(): String = lowercase().replaceFirstChar { it.uppercase() }
+ fun String.capitalize(): String = lowercase().replaceFirstChar { it.uppercase() }
/**
* Capitalize words
*/
@JvmStatic
- fun String.capitalizeWords(): String = split(" ").joinToString(" ") { it.capitalise() }
+ fun String.capitalizeWords(): String = split(" ").joinToString(" ") { it.capitalize() }
/**
* Colorize a string.
@@ -204,7 +204,7 @@ object Utils {
fun Int.isHttpSuccess() = this in 200..399
/**
- * Returns the last item of a list of strings or empty if none.
+ * Returns the last item of a list or empty if the list only has one item.
*/
@JvmStatic
fun List.lastOrEmpty(): String {
@@ -261,6 +261,32 @@ object Utils {
return if (count > 1) "${this}s" else this
}
+ /**
+ * Reads contents of a URL.
+ */
+ @JvmStatic
+ @Throws(IOException::class)
+ fun URL.reader(): UrlReaderResponse {
+ val connection = this.openConnection() as HttpURLConnection
+ try {
+ connection.setRequestProperty(
+ "User-Agent",
+ Constants.USER_AGENT
+ )
+ return if (connection.responseCode.isHttpSuccess()) {
+ UrlReaderResponse(
+ connection.responseCode,
+ connection.inputStream.bufferedReader().use { it.readText() })
+ } else {
+ UrlReaderResponse(
+ connection.responseCode,
+ connection.errorStream.bufferedReader().use { it.readText() })
+ }
+ } finally {
+ connection.disconnect()
+ }
+ }
+
/**
* Makes the given string red.
*/
@@ -305,7 +331,7 @@ object Utils {
}
/**
- * Send a formatted commands/modules, etc. list.
+ * Send a formatted list to the channel.
*/
@JvmStatic
@JvmOverloads
@@ -368,7 +394,7 @@ object Utils {
fun String.toIntOrDefault(defaultValue: Int): Int {
return try {
toInt()
- } catch (e: NumberFormatException) {
+ } catch (_: NumberFormatException) {
defaultValue
}
}
@@ -401,45 +427,19 @@ object Utils {
@JvmStatic
fun LocalDateTime.toUtcDateTime(): String = format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))
+
/**
* Makes the given string bold.
*/
@JvmStatic
fun String?.underline(): String = colorize(Colors.UNDERLINE)
-
/**
* Converts XML/XHTML entities to plain text.
*/
@JvmStatic
fun String.unescapeXml(): String = Jsoup.parse(this).text()
- /**
- * Reads contents of a URL.
- */
- @JvmStatic
- @Throws(IOException::class)
- fun URL.reader(): UrlReaderResponse {
- val connection = this.openConnection() as HttpURLConnection
- try {
- connection.setRequestProperty(
- "User-Agent",
- Constants.USER_AGENT
- )
- return if (connection.responseCode.isHttpSuccess()) {
- UrlReaderResponse(
- connection.responseCode,
- connection.inputStream.bufferedReader().use { it.readText() })
- } else {
- UrlReaderResponse(
- connection.responseCode,
- connection.errorStream.bufferedReader().use { it.readText() })
- }
- } finally {
- connection.disconnect()
- }
- }
-
/**
* Holds the [URL.reader] response code and body text.
*/
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/commands/AbstractCommand.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/AbstractCommand.kt
index 4642f42..9036163 100644
--- a/src/main/kotlin/net/thauvin/erik/mobibot/commands/AbstractCommand.kt
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/AbstractCommand.kt
@@ -38,6 +38,12 @@ import net.thauvin.erik.mobibot.Utils.sendMessage
import org.pircbotx.hooks.events.PrivateMessageEvent
import org.pircbotx.hooks.types.GenericMessageEvent
+/**
+ * Represents an abstract base class for implementing bot commands.
+ *
+ * A command is characterized by its name, visibility, access restrictions, and other properties. This class provides a
+ * framework for handling command-specific responses and help information.
+ */
abstract class AbstractCommand {
abstract val name: String
abstract val help: List
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/commands/ChannelFeed.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/ChannelFeed.kt
index 0075293..eaceb84 100644
--- a/src/main/kotlin/net/thauvin/erik/mobibot/commands/ChannelFeed.kt
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/ChannelFeed.kt
@@ -35,6 +35,9 @@ import net.thauvin.erik.mobibot.FeedReader
import net.thauvin.erik.mobibot.Utils.helpFormat
import org.pircbotx.hooks.types.GenericMessageEvent
+/**
+ * Lists the last 5 posts from the channel's weblog feed.
+ */
class ChannelFeed(channel: String) : AbstractCommand() {
override val name = channel
override val help = listOf("To list the last 5 posts from the channel's weblog feed:", helpFormat("%c $channel"))
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/commands/Cycle.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Cycle.kt
index cefcde3..bc80588 100644
--- a/src/main/kotlin/net/thauvin/erik/mobibot/commands/Cycle.kt
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Cycle.kt
@@ -39,6 +39,9 @@ import net.thauvin.erik.mobibot.Utils.helpFormat
import net.thauvin.erik.mobibot.Utils.isChannelOp
import org.pircbotx.hooks.types.GenericMessageEvent
+/**
+ * Allows an operator to have the bot leave the channel and come back.
+ */
class Cycle : AbstractCommand() {
private val wait = 10
override val name = "cycle"
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/commands/Die.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Die.kt
index d7577af..ea32ea2 100644
--- a/src/main/kotlin/net/thauvin/erik/mobibot/commands/Die.kt
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Die.kt
@@ -35,6 +35,9 @@ import net.thauvin.erik.mobibot.Utils.bot
import net.thauvin.erik.mobibot.Utils.isChannelOp
import org.pircbotx.hooks.types.GenericMessageEvent
+/**
+ * Allows an operator to terminate the bot's operations on the server.
+ */
class Die : AbstractCommand() {
override val name = "die"
override val help = emptyList()
@@ -42,6 +45,14 @@ class Die : AbstractCommand() {
override val isPublic = false
override val isVisible = false
+ companion object {
+ const val DIE_PROP = "die"
+ }
+
+ init {
+ initProperties(DIE_PROP)
+ }
+
override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
with(event.bot()) {
if (event.isChannelOp(channel) && (properties[DIE_PROP].isNullOrBlank() || args == properties[DIE_PROP])) {
@@ -51,12 +62,4 @@ class Die : AbstractCommand() {
}
}
}
-
- companion object {
- const val DIE_PROP = "die"
- }
-
- init {
- initProperties(DIE_PROP)
- }
}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/commands/Ignore.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Ignore.kt
index 13b20b0..9c3e0c1 100644
--- a/src/main/kotlin/net/thauvin/erik/mobibot/commands/Ignore.kt
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Ignore.kt
@@ -41,9 +41,23 @@ import net.thauvin.erik.mobibot.Utils.sendMessage
import net.thauvin.erik.mobibot.commands.links.LinksManager
import org.pircbotx.hooks.types.GenericMessageEvent
+/**
+ * Adds or removed nicks from the ignored list.
+ */
class Ignore : AbstractCommand() {
private val me = "me"
+ companion object {
+ const val IGNORE_CMD = "ignore"
+ const val IGNORE_PROP = IGNORE_CMD
+ private val ignored = mutableSetOf()
+
+ @JvmStatic
+ fun isNotIgnored(nick: String): Boolean {
+ return !ignored.contains(nick.lowercase())
+ }
+ }
+
init {
initProperties(IGNORE_PROP)
}
@@ -65,17 +79,6 @@ class Ignore : AbstractCommand() {
override val isPublic = true
override val isVisible = true
- companion object {
- const val IGNORE_CMD = "ignore"
- const val IGNORE_PROP = IGNORE_CMD
- private val ignored = mutableSetOf()
-
- @JvmStatic
- fun isNotIgnored(nick: String): Boolean {
- return !ignored.contains(nick.lowercase())
- }
- }
-
override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
val isMe = args.trim().equals(me, true)
if (isMe || !event.isChannelOp(channel)) {
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/commands/Info.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Info.kt
index 8e244cc..8888c9b 100644
--- a/src/main/kotlin/net/thauvin/erik/mobibot/commands/Info.kt
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Info.kt
@@ -31,7 +31,7 @@
package net.thauvin.erik.mobibot.commands
import net.thauvin.erik.mobibot.ReleaseInfo
-import net.thauvin.erik.mobibot.Utils.capitalise
+import net.thauvin.erik.mobibot.Utils.capitalize
import net.thauvin.erik.mobibot.Utils.green
import net.thauvin.erik.mobibot.Utils.helpFormat
import net.thauvin.erik.mobibot.Utils.isChannelOp
@@ -46,9 +46,12 @@ import java.lang.management.ManagementFactory
import kotlin.time.DurationUnit
import kotlin.time.toDuration
+/**
+ * Provides detailed bot and channel statistics.
+ */
class Info(private val tell: Tell, private val seen: Seen) : AbstractCommand() {
private val allVersions = listOf(
- "${ReleaseInfo.PROJECT.capitalise()} ${ReleaseInfo.VERSION} (${ReleaseInfo.WEBSITE.green()})",
+ "${ReleaseInfo.PROJECT.capitalize()} ${ReleaseInfo.VERSION} (${ReleaseInfo.WEBSITE.green()})",
"Written by ${ReleaseInfo.AUTHOR} (${ReleaseInfo.AUTHOR_URL.green()})"
)
override val name = "info"
@@ -71,30 +74,32 @@ class Info(private val tell: Tell, private val seen: Seen) : AbstractCommand() {
val weeks = days / 7
days %= 7
- with(StringBuffer()) {
+ with(mutableListOf()) {
if (years > 0) {
- append(years).append(" year".plural(years)).append(' ')
+ add("$years".plus(" year".plural(years)))
}
if (months > 0) {
- append(months).append(" month".plural(months)).append(' ')
+ add("$months".plus(" month".plural(months)))
}
if (weeks > 0) {
- append(weeks).append(" week".plural(weeks)).append(' ')
+ add("$weeks".plus(" week".plural(weeks)))
}
if (days > 0) {
- append(days).append(" day".plural(days)).append(' ')
+ add("$days".plus(" day".plural(days)))
}
if (hours > 0) {
- append(hours).append(" hour".plural(hours.toLong())).append(' ')
+ add("$hours".plus(" hour".plural(hours.toLong())))
}
if (minutes > 0) {
- append(minutes).append(" minute".plural(minutes.toLong()))
- } else {
- append(seconds).append(" second".plural(seconds.toLong()))
+ add("$minutes".plus(" minute".plural(minutes.toLong())))
+ } else if (seconds > 0) {
+ add("$seconds seconds")
+ } else if (isEmpty()) {
+ return "0 second"
}
- return toString()
+ return this.joinToString(" ")
}
}
}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/commands/Me.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Me.kt
index afa9046..2ae07a9 100644
--- a/src/main/kotlin/net/thauvin/erik/mobibot/commands/Me.kt
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Me.kt
@@ -36,6 +36,9 @@ import net.thauvin.erik.mobibot.Utils.helpFormat
import net.thauvin.erik.mobibot.Utils.isChannelOp
import org.pircbotx.hooks.types.GenericMessageEvent
+/**
+ * Allows an operator to have the bot perform an action in the specified channel.
+ */
class Me : AbstractCommand() {
override val name = "me"
override val help = listOf("To have the bot perform an action:", helpFormat("%c $name "))
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/commands/Modules.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Modules.kt
index 8668bf7..316662e 100644
--- a/src/main/kotlin/net/thauvin/erik/mobibot/commands/Modules.kt
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Modules.kt
@@ -36,6 +36,9 @@ import net.thauvin.erik.mobibot.Utils.isChannelOp
import net.thauvin.erik.mobibot.Utils.sendList
import org.pircbotx.hooks.types.GenericMessageEvent
+/**
+ * List the enabled/disabled modules.
+ */
class Modules(private val modules: List, private val disabledModules: List) : AbstractCommand() {
override val name = "modules"
override val help = listOf("To view a list of enabled/disabled modules:", helpFormat("%c $name"))
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/commands/Msg.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Msg.kt
index 14d8d8e..2d21f08 100644
--- a/src/main/kotlin/net/thauvin/erik/mobibot/commands/Msg.kt
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Msg.kt
@@ -36,6 +36,9 @@ import net.thauvin.erik.mobibot.Utils.helpFormat
import net.thauvin.erik.mobibot.Utils.isChannelOp
import org.pircbotx.hooks.types.GenericMessageEvent
+/**
+ * Allows an operator to send a private message to the specified user.
+ */
class Msg : AbstractCommand() {
override val name = "msg"
override val help = listOf(
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/commands/Nick.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Nick.kt
index 21c96b5..56eaefb 100644
--- a/src/main/kotlin/net/thauvin/erik/mobibot/commands/Nick.kt
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Nick.kt
@@ -36,6 +36,9 @@ import net.thauvin.erik.mobibot.Utils.helpFormat
import net.thauvin.erik.mobibot.Utils.isChannelOp
import org.pircbotx.hooks.types.GenericMessageEvent
+/**
+ * Allows an operator to change the bot's nickname.
+ */
class Nick : AbstractCommand() {
override val name = "nick"
override val help = listOf("To change the bot's nickname:", helpFormat("%c $name "))
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/commands/Recap.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Recap.kt
index 500fd85..4a8eab7 100644
--- a/src/main/kotlin/net/thauvin/erik/mobibot/commands/Recap.kt
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Recap.kt
@@ -38,6 +38,9 @@ import org.pircbotx.hooks.types.GenericMessageEvent
import java.time.Clock
import java.time.LocalDateTime
+/**
+ * Lists the last 10 public channel messages.
+ */
class Recap : AbstractCommand() {
override val name = "recap"
override val help = listOf(
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/commands/Say.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Say.kt
index b9d410d..28aa346 100644
--- a/src/main/kotlin/net/thauvin/erik/mobibot/commands/Say.kt
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Say.kt
@@ -36,6 +36,9 @@ import net.thauvin.erik.mobibot.Utils.helpFormat
import net.thauvin.erik.mobibot.Utils.isChannelOp
import org.pircbotx.hooks.types.GenericMessageEvent
+/**
+ * Allows an operator to have the bot say something in the specified channel.
+ */
class Say : AbstractCommand() {
override val name = "say"
override val help = listOf("To have the bot say something on the channel:", helpFormat("%c $name "))
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/commands/Users.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Users.kt
index 960b8aa..ff5ab6d 100644
--- a/src/main/kotlin/net/thauvin/erik/mobibot/commands/Users.kt
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Users.kt
@@ -36,6 +36,9 @@ import net.thauvin.erik.mobibot.Utils.helpFormat
import net.thauvin.erik.mobibot.Utils.sendList
import org.pircbotx.hooks.types.GenericMessageEvent
+/**
+ * Lists the users present on the channel.
+ */
class Users : AbstractCommand() {
override val name = "users"
override val help = listOf("To list the users present on the channel:", helpFormat("%c $name"))
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/commands/Versions.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Versions.kt
index 62cb044..d509a48 100644
--- a/src/main/kotlin/net/thauvin/erik/mobibot/commands/Versions.kt
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Versions.kt
@@ -38,6 +38,9 @@ import net.thauvin.erik.mobibot.Utils.toIsoLocalDate
import org.pircbotx.PircBotX
import org.pircbotx.hooks.types.GenericMessageEvent
+/**
+ * Lists the bot version, OS version, JVM version, Kotlin version and PircBotX version.
+ */
class Versions : AbstractCommand() {
private val allVersions = listOf(
"Version: ${ReleaseInfo.VERSION} (${ReleaseInfo.BUILD_DATE.toIsoLocalDate()})",
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/commands/links/Comment.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/links/Comment.kt
index f0d9d0c..5fea73a 100644
--- a/src/main/kotlin/net/thauvin/erik/mobibot/commands/links/Comment.kt
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/links/Comment.kt
@@ -42,6 +42,10 @@ import net.thauvin.erik.mobibot.entries.EntriesUtils.toLinkLabel
import net.thauvin.erik.mobibot.entries.EntryLink
import org.pircbotx.hooks.types.GenericMessageEvent
+/**
+ * Processes commands such as viewing, deleting, editing, or changing the author of a comment based on the input
+ * arguments and the state of the entries.
+ */
class Comment : AbstractCommand() {
override val name = COMMAND
override val help = listOf(
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/commands/links/LinksManager.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/links/LinksManager.kt
index e688092..05b8192 100644
--- a/src/main/kotlin/net/thauvin/erik/mobibot/commands/links/LinksManager.kt
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/links/LinksManager.kt
@@ -49,6 +49,12 @@ import org.jsoup.Jsoup
import org.pircbotx.hooks.types.GenericMessageEvent
import java.io.IOException
+/**
+ * Processes a URL, fetch its metadata, and register it with associated details.
+ *
+ * It checks for duplicate entries, retrieves or assigns a title, associates tags, and adds the URL to a collection of
+ * entries for further processing.
+ */
class LinksManager : AbstractCommand() {
private val defaultTags: MutableList = mutableListOf()
private val keywords: MutableList = mutableListOf()
@@ -59,10 +65,6 @@ class LinksManager : AbstractCommand() {
override val isPublic = false
override val isVisible = false
- init {
- initProperties(TAGS_PROP, KEYWORDS_PROP)
- }
-
companion object {
val LINK_MATCH = "^[hH][tT][tT][pP](|[sS])://.*".toRegex()
const val KEYWORDS_PROP = "tags-keywords"
@@ -100,6 +102,10 @@ class LinksManager : AbstractCommand() {
}
}
+ init {
+ initProperties(TAGS_PROP, KEYWORDS_PROP)
+ }
+
override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
val cmds = args.split(" ".toRegex(), 2)
val sender = event.user.nick
@@ -120,10 +126,11 @@ class LinksManager : AbstractCommand() {
}
if (title.isBlank()) {
- title = fetchTitle(link)
+ title = fetchPageTitle(link)
}
if (title != Constants.NO_TITLE) {
+ // Add keywords as tags if found in the title
matchTagKeywords(title, tags)
}
@@ -139,7 +146,7 @@ class LinksManager : AbstractCommand() {
pinboard.addPin(event.bot().serverHostname, entry)
- // Queue link for posting to social media.
+ // Queue the entry for posting to social media.
socialManager.queueEntry(index)
entries.save()
@@ -158,7 +165,12 @@ class LinksManager : AbstractCommand() {
return message.matches(LINK_MATCH)
}
- internal fun fetchTitle(link: String): String {
+ /**
+ * Fetches and returns the page title of the given URL.
+ *
+ * If the title cannot be fetched or is blank, [Constants.NO_TITLE] is returned.
+ */
+ internal fun fetchPageTitle(link: String): String {
try {
val html = Jsoup.connect(link)
.userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:74.0) Gecko/20100101 Firefox/74.0")
@@ -167,7 +179,7 @@ class LinksManager : AbstractCommand() {
if (title.isNotBlank()) {
return title
}
- } catch (ignore: IOException) {
+ } catch (_: IOException) {
// Do nothing
}
return Constants.NO_TITLE
@@ -181,12 +193,16 @@ class LinksManager : AbstractCommand() {
"Duplicate".bold() + " >> " + printLink(entries.links.indexOf(match), match)
)
true
- } catch (ignore: NoSuchElementException) {
+ } catch (_: NoSuchElementException) {
false
}
}
}
+
+ /**
+ * Matches [keywords] in the given title and adds them to the provided tag list.
+ */
internal fun matchTagKeywords(title: String, tags: MutableList) {
for (match in keywords) {
val m = Regex.escape(match)
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/commands/links/Posting.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/links/Posting.kt
index a47021b..1bbed6d 100644
--- a/src/main/kotlin/net/thauvin/erik/mobibot/commands/links/Posting.kt
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/links/Posting.kt
@@ -44,6 +44,11 @@ import net.thauvin.erik.mobibot.entries.EntriesUtils.toLinkLabel
import net.thauvin.erik.mobibot.entries.EntryLink
import org.pircbotx.hooks.types.GenericMessageEvent
+/**
+ * Modifies or displays the content of an entry.
+ *
+ * It handles actions such as adding comments, changing author, title, or URL, and removing or printing entries.
+ */
class Posting : AbstractCommand() {
override val name = "posting"
override val help = listOf(
@@ -97,6 +102,20 @@ class Posting : AbstractCommand() {
entries.save()
}
+ private fun changeAuthor(channel: String, cmd: String, index: Int, event: GenericMessageEvent) {
+ if (event.isChannelOp(channel)) {
+ if (cmd.length > 1) {
+ val entry: EntryLink = entries.links[index]
+ entry.nick = cmd.substring(1)
+ LinksManager.pinboard.updatePin(event.bot().serverHostname, entry.link, entry)
+ event.sendMessage(EntriesUtils.printLink(index, entry))
+ entries.save()
+ }
+ } else {
+ event.sendMessage("Please ask a channel op to change the author of this link for you.")
+ }
+ }
+
private fun changeTitle(cmd: String, entryIndex: Int, event: GenericMessageEvent) {
if (cmd.length > 1) {
val entry: EntryLink = entries.links[entryIndex]
@@ -121,20 +140,6 @@ class Posting : AbstractCommand() {
}
}
- private fun changeAuthor(channel: String, cmd: String, index: Int, event: GenericMessageEvent) {
- if (event.isChannelOp(channel)) {
- if (cmd.length > 1) {
- val entry: EntryLink = entries.links[index]
- entry.nick = cmd.substring(1)
- LinksManager.pinboard.updatePin(event.bot().serverHostname, entry.link, entry)
- event.sendMessage(EntriesUtils.printLink(index, entry))
- entries.save()
- }
- } else {
- event.sendMessage("Please ask a channel op to change the author of this link for you.")
- }
- }
-
private fun removeEntry(channel: String, index: Int, event: GenericMessageEvent) {
val entry: EntryLink = entries.links[index]
if (entry.login == event.user.login || event.isChannelOp(channel)) {
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/commands/links/Tags.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/links/Tags.kt
index 0d73f6e..fac834c 100644
--- a/src/main/kotlin/net/thauvin/erik/mobibot/commands/links/Tags.kt
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/links/Tags.kt
@@ -41,6 +41,12 @@ import net.thauvin.erik.mobibot.entries.EntriesUtils
import net.thauvin.erik.mobibot.entries.EntryLink
import org.pircbotx.hooks.types.GenericMessageEvent
+/**
+ * Manages tags associated with a specific link entry.
+ *
+ * Allows users to modify or view tags associated with a link. Users can only change tags for their own links unless
+ * they are channel operators.
+ */
class Tags : AbstractCommand() {
override val name = COMMAND
override val help = listOf(
@@ -55,6 +61,7 @@ class Tags : AbstractCommand() {
const val COMMAND = "tags"
}
+
override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
val cmds = args.substring(1).split("${Constants.TAG_CMD}:", limit = 2)
val index = cmds[0].toInt() - 1
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/commands/links/View.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/links/View.kt
index 6891c2d..03f68df 100644
--- a/src/main/kotlin/net/thauvin/erik/mobibot/commands/links/View.kt
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/links/View.kt
@@ -43,6 +43,9 @@ import net.thauvin.erik.mobibot.entries.EntryLink
import org.pircbotx.hooks.events.PrivateMessageEvent
import org.pircbotx.hooks.types.GenericMessageEvent
+/**
+ * Displays a list of entries or an appropriate message if no entries exist.
+ */
class View : AbstractCommand() {
override val name = VIEW_CMD
override val help = listOf(
@@ -61,12 +64,17 @@ class View : AbstractCommand() {
override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
if (entries.links.isNotEmpty()) {
val p = parseArgs(args)
- showPosts(p.first, p.second, event)
+ viewEntries(p.first, p.second, event)
} else {
event.sendMessage("There is currently nothing to view. Why don't you post something?")
}
}
+ /**
+ * Parses the view command input arguments and determines a starting index and query string.
+ *
+ *`view [] []`
+ */
internal fun parseArgs(args: String): Pair {
var query = args.lowercase().trim()
var start = 0
@@ -81,14 +89,14 @@ class View : AbstractCommand() {
if (start > entries.links.size) {
start = 0
}
- } catch (ignore: NumberFormatException) {
+ } catch (_: NumberFormatException) {
// Do nothing
}
}
return Pair(start, query)
}
- private fun showPosts(start: Int, query: String, event: GenericMessageEvent) {
+ private fun viewEntries(start: Int, query: String, event: GenericMessageEvent) {
var index = start
var entry: EntryLink
var sent = 0
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/commands/seen/NickComparator.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/seen/NickComparator.kt
index f44b357..79f7475 100644
--- a/src/main/kotlin/net/thauvin/erik/mobibot/commands/seen/NickComparator.kt
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/seen/NickComparator.kt
@@ -33,13 +33,16 @@ package net.thauvin.erik.mobibot.commands.seen
import java.io.Serializable
+/**
+ * A comparator implementation for comparing nicknames in a case-insensitive manner.
+ */
class NickComparator : Comparator, Serializable {
override fun compare(a: String, b: String): Int {
return a.lowercase().compareTo(b.lowercase())
}
companion object {
- @Suppress("ConstPropertyName")
+ @Suppress("unused")
private const val serialVersionUID = 1L
}
}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/commands/seen/Seen.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/seen/Seen.kt
index 8af98dc..9887721 100644
--- a/src/main/kotlin/net/thauvin/erik/mobibot/commands/seen/Seen.kt
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/seen/Seen.kt
@@ -49,7 +49,9 @@ import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.util.*
-
+/**
+ * Displays when a user was last seen, or all seen nicks.
+ */
class Seen(private val serialObject: String) : AbstractCommand() {
private val logger: Logger = LoggerFactory.getLogger(Seen::class.java)
private val allKeyword = "all"
@@ -64,6 +66,9 @@ class Seen(private val serialObject: String) : AbstractCommand() {
override val isPublic = true
override val isVisible = true
+ init {
+ load()
+ }
override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
if (isEnabled()) {
@@ -143,8 +148,4 @@ class Seen(private val serialObject: String) : AbstractCommand() {
fun save() {
saveSerialData(serialObject, seenNicks, logger, "seen nicknames")
}
-
- init {
- load()
- }
}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/commands/seen/SeenNick.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/seen/SeenNick.kt
index 21d7cb9..fd38e92 100644
--- a/src/main/kotlin/net/thauvin/erik/mobibot/commands/seen/SeenNick.kt
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/seen/SeenNick.kt
@@ -33,9 +33,12 @@ package net.thauvin.erik.mobibot.commands.seen
import java.io.Serializable
+/**
+ * Holds a [Seen] nickname.
+ */
data class SeenNick(val nick: String, val lastSeen: Long) : Serializable {
companion object {
- @Suppress("ConstPropertyName")
+ @Suppress("unused")
private const val serialVersionUID = 1L
}
}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/commands/tell/Tell.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/tell/Tell.kt
index 26fe803..32821d0 100644
--- a/src/main/kotlin/net/thauvin/erik/mobibot/commands/tell/Tell.kt
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/tell/Tell.kt
@@ -48,7 +48,7 @@ import org.pircbotx.hooks.types.GenericMessageEvent
import org.pircbotx.hooks.types.GenericUserEvent
/**
- * The `Tell` command.
+ * Queues a message to be sent to someone when they join or are active on the channel.
*/
class Tell(private val serialObject: String) : AbstractCommand() {
// Messages queue
@@ -284,7 +284,7 @@ class Tell(private val serialObject: String) : AbstractCommand() {
// Arrow
private const val ARROW = " --> "
- // All keyword
+ // The `all` keyword
private const val TELL_ALL_KEYWORD = "all"
// The delete command.
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/commands/tell/TellManager.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/tell/TellManager.kt
index f193a3c..1200a15 100644
--- a/src/main/kotlin/net/thauvin/erik/mobibot/commands/tell/TellManager.kt
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/tell/TellManager.kt
@@ -38,7 +38,7 @@ import java.time.Clock
import java.time.LocalDateTime
/**
- * The Tell Messages Manager.
+ * Manages the [Tell] messages queue.
*/
object TellManager {
private val logger: Logger = LoggerFactory.getLogger(TellManager::class.java)
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/commands/tell/TellMessage.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/tell/TellMessage.kt
index 1f55687..9c263eb 100644
--- a/src/main/kotlin/net/thauvin/erik/mobibot/commands/tell/TellMessage.kt
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/tell/TellMessage.kt
@@ -36,7 +36,7 @@ import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
/**
- * Tell Message.
+ * Holds a [Tell] message.
*/
class TellMessage(
/**
@@ -98,7 +98,7 @@ class TellMessage(
}
companion object {
- @Suppress("ConstPropertyName")
+ @Suppress("unused")
private const val serialVersionUID = 2L
}
}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/entries/Entries.kt b/src/main/kotlin/net/thauvin/erik/mobibot/entries/Entries.kt
index 4e187d4..c620dda 100644
--- a/src/main/kotlin/net/thauvin/erik/mobibot/entries/Entries.kt
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/entries/Entries.kt
@@ -33,6 +33,9 @@ package net.thauvin.erik.mobibot.entries
import net.thauvin.erik.mobibot.Utils.today
+/**
+ * Holds [EntryLink] entries.
+ */
class Entries(
var channel: String = "",
var ircServer: String = "",
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/entries/EntriesUtils.kt b/src/main/kotlin/net/thauvin/erik/mobibot/entries/EntriesUtils.kt
index 1588704..cac0243 100644
--- a/src/main/kotlin/net/thauvin/erik/mobibot/entries/EntriesUtils.kt
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/entries/EntriesUtils.kt
@@ -35,7 +35,7 @@ import net.thauvin.erik.mobibot.Utils.bold
import net.thauvin.erik.mobibot.Utils.green
/**
- * Entries utilities.
+ * Provides functions used to manage [Entries].
*/
object EntriesUtils {
/**
@@ -69,14 +69,14 @@ object EntriesUtils {
}
/**
- * Prints an entry's tags/categories for display on the channel. e.g. L1T: tag1, tag2
+ * Prints an entry's tags/categories for display on the channel. (e.g., L1T: tag1, tag2)
*/
@JvmStatic
fun printTags(entryIndex: Int, entry: EntryLink): String =
entryIndex.toLinkLabel() + "${Constants.TAG_CMD}: " + entry.formatTags(", ")
/**
- * Builds link label based on its index. e.g: L1
+ * Builds link label based on its index. (e.g., L1)
*/
@JvmStatic
fun Int.toLinkLabel(): String = Constants.LINK_CMD + (this + 1)
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/entries/EntryComment.kt b/src/main/kotlin/net/thauvin/erik/mobibot/entries/EntryComment.kt
index 1826101..80802a9 100644
--- a/src/main/kotlin/net/thauvin/erik/mobibot/entries/EntryComment.kt
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/entries/EntryComment.kt
@@ -34,7 +34,7 @@ import java.io.Serializable
import java.time.LocalDateTime
/**
- * Entry comments data class.
+ * [Entries] comment.
*/
data class EntryComment(var comment: String, var nick: String) : Serializable {
/**
@@ -45,8 +45,7 @@ data class EntryComment(var comment: String, var nick: String) : Serializable {
override fun toString(): String = "EntryComment{comment='$comment', date=$date, nick='$nick'}"
companion object {
- // Serial version UID
- @Suppress("ConstPropertyName")
- private const val serialVersionUID: Long = 1L
+ @Suppress("unused")
+ private const val serialVersionUID = 1L
}
}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/entries/EntryLink.kt b/src/main/kotlin/net/thauvin/erik/mobibot/entries/EntryLink.kt
index a807f07..8d026a1 100644
--- a/src/main/kotlin/net/thauvin/erik/mobibot/entries/EntryLink.kt
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/entries/EntryLink.kt
@@ -37,7 +37,7 @@ import java.io.Serializable
import java.util.*
/**
- * The class used to store link entries.
+ * Holds [Entries] link.
*/
class EntryLink(
// Link's comments
@@ -92,6 +92,11 @@ class EntryLink(
this.tags.addAll(tags)
}
+ companion object {
+ @Suppress("unused")
+ private const val serialVersionUID = 1L
+ }
+
/**
* Adds a new comment
*/
@@ -204,10 +209,4 @@ class EntryLink(
return ("EntryLink{channel='$channel', comments=$comments, date=$date, link='$link', login='$login'," +
"nick='$nick', tags=$tags, title='$title'}")
}
-
- companion object {
- // Serial version UID
- @Suppress("ConstPropertyName")
- private const val serialVersionUID: Long = 1L
- }
}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/modules/AbstractModule.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/AbstractModule.kt
index 1ced830..08af9da 100644
--- a/src/main/kotlin/net/thauvin/erik/mobibot/modules/AbstractModule.kt
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/AbstractModule.kt
@@ -37,7 +37,9 @@ import org.pircbotx.hooks.events.PrivateMessageEvent
import org.pircbotx.hooks.types.GenericMessageEvent
/**
- * The `Module` abstract class.
+ * Represents an abstract module, which can be extended to implement specific functionality.
+ *
+ * This class provides a foundation for creating modules with configurable properties, commands, and help features.
*/
abstract class AbstractModule {
/**
@@ -76,6 +78,7 @@ abstract class AbstractModule {
/**
* Responds with the module's help.
*/
+ @Suppress("SameReturnValue")
open fun helpResponse(event: GenericMessageEvent): Boolean {
for (h in help) {
event.sendMessage(helpCmdSyntax(h, event.bot().nick, isPrivateMsgEnabled && event is PrivateMessageEvent))
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/modules/Calc.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/Calc.kt
index 7fd320f..fd60c34 100644
--- a/src/main/kotlin/net/thauvin/erik/mobibot/modules/Calc.kt
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/Calc.kt
@@ -40,35 +40,19 @@ import org.slf4j.LoggerFactory
import java.text.DecimalFormat
/**
- * The Calc module.
+ * Performs a calculation.
*/
class Calc : AbstractModule() {
private val logger: Logger = LoggerFactory.getLogger(Calc::class.java)
override val name = "Calc"
- override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
- if (args.isNotBlank()) {
- try {
- event.respond(calculate(args))
- } catch (e: IllegalArgumentException) {
- if (logger.isWarnEnabled) logger.warn("Failed to calculate: $args", e)
- event.respond("No idea. This is the kind of math I don't get.")
- } catch (e: UnknownFunctionOrVariableException) {
- if (logger.isWarnEnabled) logger.warn("Unable to calculate: $args", e)
- event.respond("No idea. I must've some form of Dyscalculia.")
- }
- } else {
- helpResponse(event)
- }
- }
-
companion object {
// Calc command
private const val CALC_CMD = "calc"
/**
- * Performs a calculation. e.g.: 1 + 1 * 2
+ * Performs a calculation (e.g.: 1 + 1 * 2)
*/
@JvmStatic
@Throws(IllegalArgumentException::class)
@@ -84,4 +68,20 @@ class Calc : AbstractModule() {
help.add("To solve a mathematical calculation:")
help.add(helpFormat("%c $CALC_CMD "))
}
+
+ override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
+ if (args.isNotBlank()) {
+ try {
+ event.respond(calculate(args))
+ } catch (e: UnknownFunctionOrVariableException) {
+ if (logger.isWarnEnabled) logger.warn("Unable to calculate: $args", e)
+ event.respond("No idea. This is the kind of math I don't get.")
+ } catch (e: IllegalArgumentException) {
+ if (logger.isWarnEnabled) logger.warn("Failed to calculate: $args", e)
+ event.respond("No idea. I must've some form of Dyscalculia.")
+ }
+ } else {
+ helpResponse(event)
+ }
+ }
}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/modules/ChatGpt2.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/ChatGpt2.kt
index fbf0e94..c31cbaf 100644
--- a/src/main/kotlin/net/thauvin/erik/mobibot/modules/ChatGpt2.kt
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/ChatGpt2.kt
@@ -39,37 +39,14 @@ import org.pircbotx.hooks.types.GenericMessageEvent
import org.slf4j.Logger
import org.slf4j.LoggerFactory
+/**
+ * Allows user to interact with ChatGPT.
+ */
class ChatGpt2 : AbstractModule() {
val logger: Logger = LoggerFactory.getLogger(ChatGpt2::class.java)
override val name = CHATGPT_NAME
- override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
- if (args.isNotBlank()) {
- try {
- val answer = chat(
- args.trim(), properties[API_KEY_PROP],
- properties.getOrDefault(MAX_TOKENS_PROP, "1024").toInt()
- )
- if (answer.isNotBlank()) {
- event.sendMessage(answer)
- } else {
- event.respond("$name is stumped.")
- }
- } catch (e: ModuleException) {
- if (logger.isWarnEnabled) logger.warn(e.debugMessage, e)
- e.message?.let {
- event.respond(it)
- }
- } catch (e: NumberFormatException) {
- if (logger.isErrorEnabled) logger.error("Invalid $MAX_TOKENS_PROP property.", e)
- event.respond("The $name module is misconfigured.")
- }
- } else {
- helpResponse(event)
- }
- }
-
companion object {
/**
* The service name.
@@ -82,7 +59,7 @@ class ChatGpt2 : AbstractModule() {
const val API_KEY_PROP = "chatgpt-api-key"
/**
- * The max tokens property.
+ * The max-tokens property.
*/
const val MAX_TOKENS_PROP = "chatgpt-max-tokens"
@@ -125,4 +102,33 @@ class ChatGpt2 : AbstractModule() {
}
initProperties(API_KEY_PROP, MAX_TOKENS_PROP)
}
+
+ /**
+ * Gets answers by chatting with ChatGPT.
+ */
+ override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
+ if (args.isNotBlank()) {
+ try {
+ val answer = chat(
+ args.trim(), properties[API_KEY_PROP],
+ properties.getOrDefault(MAX_TOKENS_PROP, "1024").toInt()
+ )
+ if (answer.isNotBlank()) {
+ event.sendMessage(answer)
+ } else {
+ event.respond("$name is stumped.")
+ }
+ } catch (e: ModuleException) {
+ if (logger.isWarnEnabled) logger.warn(e.debugMessage, e)
+ e.message?.let {
+ event.respond(it)
+ }
+ } catch (e: NumberFormatException) {
+ if (logger.isErrorEnabled) logger.error("Invalid $MAX_TOKENS_PROP property.", e)
+ event.respond("The $name module is misconfigured.")
+ }
+ } else {
+ helpResponse(event)
+ }
+ }
}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/modules/CryptoPrices.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/CryptoPrices.kt
index 3334a90..a913a40 100644
--- a/src/main/kotlin/net/thauvin/erik/mobibot/modules/CryptoPrices.kt
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/CryptoPrices.kt
@@ -43,54 +43,13 @@ import org.slf4j.LoggerFactory
import java.io.IOException
/**
- * The Cryptocurrency Prices module.
+ * Retrieves cryptocurrency market prices.
*/
class CryptoPrices : AbstractModule() {
private val logger: Logger = LoggerFactory.getLogger(CryptoPrices::class.java)
override val name = "CryptoPrices"
- /**
- * Returns the cryptocurrency market price from
- * [Coinbase](https://docs.cdp.coinbase.com/coinbase-app/docs/api-prices#get-spot-price).
- */
- override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
- if (CURRENCIES.isEmpty()) {
- try {
- loadCurrencies()
- } catch (e: ModuleException) {
- if (logger.isWarnEnabled) logger.warn(e.debugMessage, e)
- }
- }
-
- val debugMessage = "crypto($cmd $args)"
- if (args == CODES_KEYWORD) {
- event.sendMessage("The supported currencies are:")
- event.sendList(ArrayList(CURRENCIES.keys), 10, isIndent = true)
- } else if (args.matches("\\w+( [a-zA-Z]{3}+)?".toRegex())) {
- try {
- val price = currentPrice(args.split(' '))
- val amount = try {
- price.toCurrency()
- } catch (ignore: IllegalArgumentException) {
- price.amount
- }
- event.respond("${price.base} current price is $amount [${CURRENCIES[price.currency]}]")
- } catch (e: CryptoException) {
- if (logger.isWarnEnabled) logger.warn("$debugMessage => ${e.statusCode}", e)
- e.message?.let {
- event.respond(it)
- }
- } catch (e: IOException) {
- if (logger.isErrorEnabled) logger.error(debugMessage, e)
- event.respond("An IO error has occurred while retrieving the cryptocurrency market price.")
- }
- } else {
- helpResponse(event)
- }
-
- }
-
companion object {
// Crypto command
private const val CRYPTO_CMD = "crypto"
@@ -101,8 +60,11 @@ class CryptoPrices : AbstractModule() {
// Currency codes keyword
private const val CODES_KEYWORD = "codes"
+ // Default error message
+ const val DEFAULT_ERROR_MESSAGE = "An error has occurred while retrieving the cryptocurrency market price"
+
/**
- * Get current market price.
+ * Get the current market price.
*/
@JvmStatic
fun currentPrice(args: List): CryptoPrice {
@@ -156,4 +118,47 @@ class CryptoPrices : AbstractModule() {
}
loadCurrencies()
}
+
+ /**
+ * Returns the cryptocurrency market price from
+ * [Coinbase](https://docs.cdp.coinbase.com/coinbase-app/docs/api-prices#get-spot-price).
+ */
+ override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
+ if (CURRENCIES.isEmpty()) {
+ try {
+ loadCurrencies()
+ } catch (e: ModuleException) {
+ if (logger.isWarnEnabled) logger.warn(e.debugMessage, e)
+ }
+ }
+
+ val debugMessage = "crypto($cmd $args)"
+ if (args == CODES_KEYWORD) {
+ event.sendMessage("The supported currencies are:")
+ event.sendList(ArrayList(CURRENCIES.keys), 10, isIndent = true)
+ } else if (args.matches("\\w+( [a-zA-Z]{3}+)?".toRegex())) {
+ try {
+ val price = currentPrice(args.split(' '))
+ val amount = try {
+ price.toCurrency()
+ } catch (_: IllegalArgumentException) {
+ price.amount
+ }
+ event.respond("${price.base} current price is $amount [${CURRENCIES[price.currency]}]")
+ } catch (e: CryptoException) {
+ if (logger.isWarnEnabled) logger.warn("$debugMessage => ${e.statusCode}", e)
+ if (e.message != null) {
+ event.respond("$DEFAULT_ERROR_MESSAGE: ${e.message}")
+ } else {
+ event.respond("$DEFAULT_ERROR_MESSAGE.")
+ }
+ } catch (e: IOException) {
+ if (logger.isErrorEnabled) logger.error(debugMessage, e)
+ event.respond("$DEFAULT_ERROR_MESSAGE: ${e.message}")
+ }
+ } else {
+ helpResponse(event)
+ }
+
+ }
}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/modules/CurrencyConverter.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/CurrencyConverter.kt
index 2ff4715..5fdad95 100644
--- a/src/main/kotlin/net/thauvin/erik/mobibot/modules/CurrencyConverter.kt
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/CurrencyConverter.kt
@@ -48,80 +48,12 @@ import java.net.URL
import java.text.DecimalFormat
import java.util.*
-
/**
- * The CurrencyConverter module.
+ * Converts between currencies.
*/
class CurrencyConverter : AbstractModule() {
- private val logger: Logger = LoggerFactory.getLogger(CurrencyConverter::class.java)
-
override val name = "CurrencyConverter"
- // Reload currency codes
- private fun reload(apiKey: String?) {
- if (!apiKey.isNullOrEmpty() && SYMBOLS.isEmpty()) {
- try {
- loadSymbols(apiKey)
- } catch (e: ModuleException) {
- if (logger.isWarnEnabled) logger.warn(e.debugMessage, e)
- }
- }
- }
-
- /**
- * Converts the specified currencies.
- */
- override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
- reload(properties[API_KEY_PROP])
-
- when {
- SYMBOLS.isEmpty() -> {
- event.respond(EMPTY_SYMBOLS_TABLE)
- }
-
- args.matches("\\d+([,\\d]+)?(\\.\\d+)? [a-zA-Z]{3}+ (to|in) [a-zA-Z]{3}+".toRegex()) -> {
- val msg = convertCurrency(properties[API_KEY_PROP], args)
- event.respond(msg.msg)
- if (msg.isError) {
- helpResponse(event)
- }
- }
-
- args.contains(CODES_KEYWORD) -> {
- event.sendMessage("The supported currency codes are:")
- event.sendList(SYMBOLS.keys.toList(), 11, isIndent = true)
- }
-
- else -> {
- helpResponse(event)
- }
- }
- }
-
- override fun helpResponse(event: GenericMessageEvent): Boolean {
- reload(properties[API_KEY_PROP])
-
- 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(
- helpFormat(
- helpCmdSyntax("%c $CURRENCY_CMD 50,000 GBP to USD", nick, isPrivateMsgEnabled)
- )
- )
- event.sendMessage("To list the supported currency codes:")
- event.sendMessage(
- helpFormat(
- helpCmdSyntax("%c $CURRENCY_CMD $CODES_KEYWORD", nick, isPrivateMsgEnabled)
- )
- )
- }
- return true
- }
-
companion object {
/**
* The API Key property.
@@ -134,14 +66,23 @@ class CurrencyConverter : AbstractModule() {
// Currency codes keyword
private const val CODES_KEYWORD = "codes"
+ // Decimal format
+ private val DECIMAL_FORMAT = DecimalFormat("0.00#")
+
// Empty symbols table.
private const val EMPTY_SYMBOLS_TABLE = "Sorry, but the currency table is empty."
+ // Logger
+ private val LOGGER: Logger = LoggerFactory.getLogger(CurrencyConverter::class.java)
+
// Currency symbols
private val SYMBOLS: TreeMap = TreeMap()
- // Decimal format
- private val DECIMAL_FORMAT = DecimalFormat("0.00#")
+ /**
+ * No API key error message.
+ */
+ const val ERROR_MESSAGE_NO_API_KEY = "No Exchange Rate API key specified."
+
/**
* Converts from a currency to another.
@@ -149,7 +90,7 @@ class CurrencyConverter : AbstractModule() {
@JvmStatic
fun convertCurrency(apiKey: String?, query: String): Message {
if (apiKey.isNullOrEmpty()) {
- throw ModuleException("${CURRENCY_CMD}($query)", "No Exchange Rate API key specified.")
+ throw ModuleException("${CURRENCY_CMD}($query)", ERROR_MESSAGE_NO_API_KEY)
}
val cmds = query.split(" ")
@@ -174,7 +115,10 @@ class CurrencyConverter : AbstractModule() {
} else {
ErrorMessage("Sorry, an error occurred while converting the currencies.")
}
- } catch (ignore: IOException) {
+ } catch (ioe: IOException) {
+ if (LOGGER.isWarnEnabled) {
+ LOGGER.warn("IO error while converting currencies: ${ioe.message}", ioe)
+ }
ErrorMessage("Sorry, an IO error occurred while converting the currencies.")
}
} else {
@@ -219,4 +163,74 @@ class CurrencyConverter : AbstractModule() {
initProperties(API_KEY_PROP)
loadSymbols(properties[ChatGpt2.API_KEY_PROP])
}
+
+ // Reload currency codes
+ private fun reload(apiKey: String?) {
+ if (!apiKey.isNullOrEmpty() && SYMBOLS.isEmpty()) {
+ try {
+ loadSymbols(apiKey)
+ } catch (e: ModuleException) {
+ if (LOGGER.isWarnEnabled) LOGGER.warn(e.debugMessage, e)
+ }
+ }
+ }
+
+ /**
+ * Converts the specified currencies.
+ */
+ override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
+ reload(properties[API_KEY_PROP])
+ when {
+ SYMBOLS.isEmpty() -> {
+ event.respond(EMPTY_SYMBOLS_TABLE)
+ }
+
+ args.matches("\\d+([,\\d]+)?(\\.\\d+)? [a-zA-Z]{3}+ (to|in) [a-zA-Z]{3}+".toRegex()) -> {
+ try {
+ val msg = convertCurrency(properties[API_KEY_PROP], args)
+ if (msg.isError) {
+ helpResponse(event)
+ } else {
+ event.respond(msg.msg)
+ }
+ } catch (e: ModuleException) {
+ if (LOGGER.isWarnEnabled) LOGGER.warn(e.debugMessage, e)
+ event.respond(e.message)
+ }
+ }
+
+ args.contains(CODES_KEYWORD) -> {
+ event.sendMessage("The supported currency codes are:")
+ event.sendList(SYMBOLS.keys.toList(), 11, isIndent = true)
+ }
+
+ else -> {
+ helpResponse(event)
+ }
+ }
+ }
+
+ override fun helpResponse(event: GenericMessageEvent): Boolean {
+ reload(properties[API_KEY_PROP])
+
+ 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(
+ helpFormat(
+ helpCmdSyntax("%c $CURRENCY_CMD 50,000 GBP to USD", nick, isPrivateMsgEnabled)
+ )
+ )
+ event.sendMessage("To list the supported currency codes:")
+ event.sendMessage(
+ helpFormat(
+ helpCmdSyntax("%c $CURRENCY_CMD $CODES_KEYWORD", nick, isPrivateMsgEnabled)
+ )
+ )
+ }
+ return true
+ }
}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/modules/Dice.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/Dice.kt
index 5c1dd09..54cc4c6 100644
--- a/src/main/kotlin/net/thauvin/erik/mobibot/modules/Dice.kt
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/Dice.kt
@@ -35,22 +35,11 @@ import net.thauvin.erik.mobibot.Utils.helpFormat
import org.pircbotx.hooks.types.GenericMessageEvent
/**
- * The Dice module.
+ * Rolls dice.
*/
class Dice : AbstractModule() {
override val name = "Dice"
- override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
- val arg = if (args.isBlank()) "2d6" else args.trim()
- val match = Regex("^([1-9]|[12]\\d|3[0-2])[dD]([1-9]|[12]\\d|3[0-2])$").find(arg)
- if (match != null) {
- val (dice, sides) = match.destructured
- event.respond("you rolled " + roll(dice.toInt(), sides.toInt()))
- } else {
- helpResponse(event)
- }
- }
-
companion object {
// Dice command
private const val DICE_CMD = "dice"
@@ -84,4 +73,15 @@ class Dice : AbstractModule() {
help.add("To roll 2 dice with 6 sides:")
help.add(helpFormat("%c $DICE_CMD [2d6]"))
}
+
+ override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
+ val arg = if (args.isBlank()) "2d6" else args.trim()
+ val match = Regex("^([1-9]|[12]\\d|3[0-2])[dD]([1-9]|[12]\\d|3[0-2])$").find(arg)
+ if (match != null) {
+ val (dice, sides) = match.destructured
+ event.respond("you rolled " + roll(dice.toInt(), sides.toInt()))
+ } else {
+ helpResponse(event)
+ }
+ }
}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/modules/Gemini2.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/Gemini2.kt
index e85ea7b..a690e26 100644
--- a/src/main/kotlin/net/thauvin/erik/mobibot/modules/Gemini2.kt
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/Gemini2.kt
@@ -37,47 +37,35 @@ import net.thauvin.erik.mobibot.Utils.sendMessage
import org.pircbotx.hooks.types.GenericMessageEvent
import org.slf4j.Logger
import org.slf4j.LoggerFactory
-import java.util.*
+/**
+ * Allows user to interact with Gemini.
+ */
class Gemini2 : AbstractModule() {
private val logger: Logger = LoggerFactory.getLogger(Gemini2::class.java)
override val name = GEMINI_NAME
- override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
- if (args.isNotBlank()) {
- try {
- val answer = chat(
- args.trim(),
- properties[GEMINI_API_KEY],
- properties.getOrDefault(MAX_TOKENS_PROP, "1024").toInt()
- )
- if (!answer.isNullOrEmpty()) {
- event.sendMessage(answer)
- } else {
- event.respond("$name is stumped.")
- }
- } catch (e: ModuleException) {
- if (logger.isWarnEnabled) logger.warn(e.debugMessage, e)
- e.message?.let {
- event.respond(it)
- }
- }
- } else {
- helpResponse(event)
- }
- }
-
companion object {
+ /**
+ * API Key error message
+ */
+ const val API_KEY_ERROR = "Please specify an API key."
+
+ /**
+ * The API key
+ */
+ const val GEMINI_API_KEY = "gemini-api-key"
+
/**
* The service name.
*/
const val GEMINI_NAME = "Gemini"
/**
- * The API key
+ * IO error message
*/
- const val GEMINI_API_KEY = "gemini-api-key"
+ const val IO_ERROR = "An IO error has occurred while conversing with $GEMINI_NAME."
/**
* The max number of output tokens property.
@@ -104,14 +92,10 @@ class Gemini2 : AbstractModule() {
return gemini.generate(query)
} catch (e: Exception) {
- throw ModuleException(
- "$GEMINI_CMD($query): IO",
- "An IO error has occurred while conversing with ${GEMINI_NAME}.",
- e
- )
+ throw ModuleException("$GEMINI_CMD($query): IO", IO_ERROR, e)
}
} else {
- throw ModuleException("${GEMINI_CMD}($query)", "No $GEMINI_NAME Project ID or Location specified.")
+ throw ModuleException("${GEMINI_CMD}($query)", API_KEY_ERROR)
}
}
}
@@ -127,4 +111,31 @@ class Gemini2 : AbstractModule() {
}
initProperties(GEMINI_API_KEY, MAX_TOKENS_PROP)
}
+
+ override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
+ if (args.isNotBlank()) {
+ try {
+ val answer = chat(
+ args.trim(),
+ properties[GEMINI_API_KEY],
+ properties.getOrDefault(MAX_TOKENS_PROP, "1024").toInt()
+ )
+ if (!answer.isNullOrEmpty()) {
+ event.sendMessage(answer)
+ } else {
+ event.respond("$name is stumped.")
+ }
+ } catch (e: ModuleException) {
+ if (logger.isWarnEnabled) logger.warn(e.debugMessage, e)
+ e.message?.let {
+ event.respond(it)
+ }
+ } catch (e: NumberFormatException) {
+ if (logger.isErrorEnabled) logger.error("Invalid $MAX_TOKENS_PROP property.", e)
+ event.respond("The $name module is misconfigured.")
+ }
+ } else {
+ helpResponse(event)
+ }
+ }
}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/modules/GoogleSearch.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/GoogleSearch.kt
index 26f3e71..4434f94 100644
--- a/src/main/kotlin/net/thauvin/erik/mobibot/modules/GoogleSearch.kt
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/GoogleSearch.kt
@@ -31,7 +31,6 @@
package net.thauvin.erik.mobibot.modules
import net.thauvin.erik.mobibot.ReleaseInfo
-import net.thauvin.erik.mobibot.Utils.capitalise
import net.thauvin.erik.mobibot.Utils.colorize
import net.thauvin.erik.mobibot.Utils.encodeUrl
import net.thauvin.erik.mobibot.Utils.helpFormat
@@ -51,50 +50,29 @@ import java.io.IOException
import java.net.URL
/**
- * The GoogleSearch module.
+ * Allows user to search Google.
*/
class GoogleSearch : AbstractModule() {
private val logger: Logger = LoggerFactory.getLogger(GoogleSearch::class.java)
- override val name = "GoogleSearch"
-
- /**
- * Searches Google.
- */
- override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
- if (args.isNotBlank()) {
- try {
- val results = searchGoogle(
- args,
- properties[API_KEY_PROP],
- properties[CSE_KEY_PROP],
- event.user.nick
- )
- for (msg in results) {
- if (msg.isError) {
- event.respond(msg.msg.colorize(msg.color))
- } else {
- event.sendMessage(channel, msg)
- }
- }
- } catch (e: ModuleException) {
- if (logger.isWarnEnabled) logger.warn(e.debugMessage, e)
- e.message?.let {
- event.respond(it)
- }
- }
- } else {
- helpResponse(event)
- }
- }
+ override val name = SERVICE_NAME
companion object {
- // Google API Key property
+ /**
+ * API Key property
+ */
const val API_KEY_PROP = "google-api-key"
- // Google Custom Search Engine ID property
+ /**
+ * Google Custom Search Engine ID property
+ */
const val CSE_KEY_PROP = "google-cse-cx"
+ /**
+ * The service name
+ */
+ const val SERVICE_NAME = "GoogleSearch"
+
// Google command
private const val GOOGLE_CMD = "google"
@@ -112,7 +90,7 @@ class GoogleSearch : AbstractModule() {
if (apiKey.isNullOrBlank() || cseKey.isNullOrBlank()) {
throw ModuleException(
"${GoogleSearch::class.java.name} is disabled.",
- "${GOOGLE_CMD.capitalise()} is disabled. The API keys are missing."
+ "$SERVICE_NAME is disabled. The API keys are missing."
)
}
val results = mutableListOf()
@@ -159,4 +137,34 @@ class GoogleSearch : AbstractModule() {
help.add(helpFormat("%c $GOOGLE_CMD "))
initProperties(API_KEY_PROP, CSE_KEY_PROP)
}
+
+ /**
+ * Searches Google.
+ */
+ override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
+ if (args.isNotBlank()) {
+ try {
+ val results = searchGoogle(
+ args,
+ properties[API_KEY_PROP],
+ properties[CSE_KEY_PROP],
+ event.user.nick
+ )
+ for (msg in results) {
+ if (msg.isError) {
+ event.respond(msg.msg.colorize(msg.color))
+ } else {
+ event.sendMessage(channel, msg)
+ }
+ }
+ } catch (e: ModuleException) {
+ if (logger.isWarnEnabled) logger.warn(e.debugMessage, e)
+ e.message?.let {
+ event.respond(it)
+ }
+ }
+ } else {
+ helpResponse(event)
+ }
+ }
}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/modules/Joke.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/Joke.kt
index e792ed4..f65a526 100644
--- a/src/main/kotlin/net/thauvin/erik/mobibot/modules/Joke.kt
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/Joke.kt
@@ -47,31 +47,13 @@ import org.slf4j.LoggerFactory
import java.io.IOException
/**
- * The Joke module.
+ * Displays jokes from [JokeAPI](https://v2.jokeapi.dev/).
*/
class Joke : AbstractModule() {
private val logger: Logger = LoggerFactory.getLogger(Joke::class.java)
override val name = "Joke"
- /**
- * Returns a random joke from [JokeAPI](https://v2.jokeapi.dev/).
- */
- override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
- with(event.bot()) {
- try {
- randomJoke().forEach {
- sendIRC().notice(channel, it.msg.colorize(it.color))
- }
- } catch (e: ModuleException) {
- if (logger.isWarnEnabled) logger.warn(e.debugMessage, e)
- e.message?.let {
- event.respond(it)
- }
- }
- }
- }
-
companion object {
// Joke command
private const val JOKE_CMD = "joke"
@@ -102,4 +84,20 @@ class Joke : AbstractModule() {
help.add("To display a random joke:")
help.add(helpFormat("%c $JOKE_CMD"))
}
+
+ /**
+ * Returns a random joke from [JokeAPI](https://v2.jokeapi.dev/).
+ */
+ override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
+ try {
+ randomJoke().forEach {
+ event.bot().sendIRC().notice(channel, it.msg.colorize(it.color))
+ }
+ } catch (e: ModuleException) {
+ if (logger.isWarnEnabled) logger.warn(e.debugMessage, e)
+ e.message?.let {
+ event.respond(it)
+ }
+ }
+ }
}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/modules/Lookup.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/Lookup.kt
index f700757..de60aa0 100644
--- a/src/main/kotlin/net/thauvin/erik/mobibot/modules/Lookup.kt
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/Lookup.kt
@@ -42,57 +42,13 @@ import java.net.InetAddress
import java.net.UnknownHostException
/**
- * The Lookup module.
+ * Performs a DNS lookup or Whois IP query.
*/
class Lookup : AbstractModule() {
private val logger: Logger = LoggerFactory.getLogger(Lookup::class.java)
override val name = "Lookup"
- override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
- if (args.matches("(\\S.)+(\\S)+".toRegex())) {
- try {
- event.respondWith(nslookup(args).prependIndent())
- } catch (ignore: UnknownHostException) {
- if (args.matches(
- ("(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)")
- .toRegex()
- )
- ) {
- try {
- val lines = whois(args)
- if (lines.isNotEmpty()) {
- var line: String
- var hasData = false
- for (rawLine in lines) {
- line = rawLine.trim()
- if (line.matches("^\\b(?!\\b[Cc]omment\\b)\\w+\\b: .*$".toRegex())) {
- if (!hasData) {
- event.respondWith(line)
- hasData = true
- } else {
- event.bot().sendIRC().notice(event.user.nick, line)
- }
- }
- }
- } else {
- event.respond("Unknown host.")
- }
- } catch (ioe: IOException) {
- if (logger.isWarnEnabled) {
- logger.warn("Unable to perform whois IP lookup: $args", ioe)
- }
- event.respond("Unable to perform whois IP lookup: ${ioe.message}")
- }
- } else {
- event.respond("Unknown host.")
- }
- }
- } else {
- helpResponse(event)
- }
- }
-
companion object {
/**
* The whois default host.
@@ -168,4 +124,48 @@ class Lookup : AbstractModule() {
help.add("To perform a DNS lookup query:")
help.add(helpFormat("%c $LOOKUP_CMD "))
}
+
+ override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
+ if (args.matches("(\\S.)+(\\S)+".toRegex())) {
+ try {
+ event.respondWith(nslookup(args).prependIndent())
+ } catch (_: UnknownHostException) {
+ if (args.matches(
+ ("(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)")
+ .toRegex()
+ )
+ ) {
+ try {
+ val lines = whois(args)
+ if (lines.isNotEmpty()) {
+ var line: String
+ var hasData = false
+ for (rawLine in lines) {
+ line = rawLine.trim()
+ if (line.matches("^\\b(?!\\b[Cc]omment\\b)\\w+\\b: .*$".toRegex())) {
+ if (!hasData) {
+ event.respondWith(line)
+ hasData = true
+ } else {
+ event.bot().sendIRC().notice(event.user.nick, line)
+ }
+ }
+ }
+ } else {
+ event.respond("Unknown host.")
+ }
+ } catch (ioe: IOException) {
+ if (logger.isWarnEnabled) {
+ logger.warn("Unable to perform whois IP lookup: $args", ioe)
+ }
+ event.respond("Unable to perform whois IP lookup: ${ioe.message}")
+ }
+ } else {
+ event.respond("Unknown host.")
+ }
+ }
+ } else {
+ helpResponse(event)
+ }
+ }
}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/modules/Mastodon.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/Mastodon.kt
index d4c2614..a7f7fe2 100644
--- a/src/main/kotlin/net/thauvin/erik/mobibot/modules/Mastodon.kt
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/Mastodon.kt
@@ -44,6 +44,9 @@ import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
+/**
+ * Allows users to post on Mastodon.
+ */
class Mastodon : SocialModule() {
override val name = "Mastodon"
@@ -56,6 +59,77 @@ class Mastodon : SocialModule() {
override val isValidProperties: Boolean
get() = !(properties[INSTANCE_PROP].isNullOrBlank() || properties[ACCESS_TOKEN_PROP].isNullOrBlank())
+ companion object {
+ // Property keys
+ const val ACCESS_TOKEN_PROP = "mastodon-access-token"
+ const val AUTO_POST_PROP = "mastodon-auto-post"
+ const val HANDLE_PROP = "mastodon-handle"
+ const val INSTANCE_PROP = "mastodon-instance"
+
+ private const val MASTODON_CMD = "mastodon"
+ private const val TOOT_CMD = "toot"
+
+ /**
+ * Post on Mastodon.
+ */
+ @JvmStatic
+ @Throws(ModuleException::class)
+ fun toot(accessToken: String?, instance: String?, handle: String?, message: String, isDm: Boolean): String {
+ if (accessToken.isNullOrBlank()) {
+ throw ModuleException("Missing access token", "The access token is missing.")
+ } else if (instance.isNullOrBlank()) {
+ throw ModuleException("Missing instance", "The Mastodon instance is missing.")
+ } else if (isDm && handle.isNullOrBlank()) {
+ throw ModuleException("Missing handle", "The Mastodon handle is missing.")
+ }
+ val request = HttpRequest.newBuilder()
+ .uri(URI.create("https://$instance/api/v1/statuses"))
+ .header("Content-Type", "application/json")
+ .header("Authorization", "Bearer $accessToken")
+ .POST(
+ HttpRequest.BodyPublishers.ofString(
+ JSONWriter.valueToString(
+ if (isDm) {
+ mapOf("status" to "${handle?.prefixIfMissing('@')} $message", "visibility" to "direct")
+ } else {
+ mapOf("status" to message)
+ }
+ )
+ )
+ ).build()
+ try {
+ val response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString())
+ if (response.statusCode() == 200) {
+ return try {
+ val jsonResponse = JSONObject(response.body())
+ if (isDm) {
+ jsonResponse.getString("content")
+ } else {
+ "Your message was posted to ${jsonResponse.getString("url")}"
+ }
+ } catch (e: JSONException) {
+ throw ModuleException("mastodonPost($message)", "A JSON error has occurred: ${e.message}", e)
+ }
+ } else {
+ throw IOException("HTTP Status Code: " + response.statusCode())
+ }
+ } catch (e: IOException) {
+ throw ModuleException("mastodonPost($message)", "An IO error has occurred: ${e.message}", e)
+ } catch (e: InterruptedException) {
+ throw ModuleException("mastodonPost($message)", "An error has occurred: ${e.message}", e)
+ }
+ }
+ }
+
+ init {
+ commands.add(MASTODON_CMD)
+ commands.add(TOOT_CMD)
+ help.add("To toot on Mastodon:")
+ help.add(Utils.helpFormat("%c $TOOT_CMD "))
+ properties[AUTO_POST_PROP] = "false"
+ initProperties(ACCESS_TOKEN_PROP, HANDLE_PROP, INSTANCE_PROP)
+ }
+
/**
* Formats the entry for posting.
*/
@@ -74,76 +148,11 @@ class Mastodon : SocialModule() {
@Throws(ModuleException::class)
override fun post(message: String, isDm: Boolean): String {
return toot(
- apiKey = properties[ACCESS_TOKEN_PROP],
+ accessToken = properties[ACCESS_TOKEN_PROP],
instance = properties[INSTANCE_PROP],
handle = handle,
message = message,
isDm = isDm
)
}
-
- companion object {
- // Property keys
- const val ACCESS_TOKEN_PROP = "mastodon-access-token"
- const val AUTO_POST_PROP = "mastodon-auto-post"
- const val HANDLE_PROP = "mastodon-handle"
- const val INSTANCE_PROP = "mastodon-instance"
-
- private const val MASTODON_CMD = "mastodon"
- private const val TOOT_CMD = "toot"
-
- /**
- * Post on Mastodon.
- */
- @JvmStatic
- @Throws(ModuleException::class)
- fun toot(apiKey: String?, instance: String?, handle: String?, message: String, isDm: Boolean): String {
- val request = HttpRequest.newBuilder()
- .uri(URI.create("https://$instance/api/v1/statuses"))
- .header("Content-Type", "application/json")
- .header("Authorization", "Bearer $apiKey")
- .POST(
- HttpRequest.BodyPublishers.ofString(
- JSONWriter.valueToString(
- if (isDm) {
- mapOf("status" to "${handle?.prefixIfMissing('@')} $message", "visibility" to "direct")
- } else {
- mapOf("status" to message)
- }
- )
- )
- )
- .build()
- try {
- val response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString())
- if (response.statusCode() == 200) {
- return try {
- val jsonResponse = JSONObject(response.body())
- if (isDm) {
- jsonResponse.getString("content")
- } else {
- "Your message was posted to ${jsonResponse.getString("url")}"
- }
- } catch (e: JSONException) {
- throw ModuleException("mastodonPost($message)", "A JSON error has occurred: ${e.message}", e)
- }
- } else {
- throw IOException("Status Code: " + response.statusCode())
- }
- } catch (e: IOException) {
- throw ModuleException("mastodonPost($message)", "An IO error has occurred: ${e.message}", e)
- } catch (e: InterruptedException) {
- throw ModuleException("mastodonPost($message)", "An error has occurred: ${e.message}", e)
- }
- }
- }
-
- init {
- commands.add(MASTODON_CMD)
- commands.add(TOOT_CMD)
- help.add("To toot on Mastodon:")
- help.add(Utils.helpFormat("%c $TOOT_CMD "))
- properties[AUTO_POST_PROP] = "false"
- initProperties(ACCESS_TOKEN_PROP, HANDLE_PROP, INSTANCE_PROP)
- }
}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/modules/ModuleException.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/ModuleException.kt
index 26d374a..2f6d4e3 100644
--- a/src/main/kotlin/net/thauvin/erik/mobibot/modules/ModuleException.kt
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/ModuleException.kt
@@ -30,16 +30,13 @@
*/
package net.thauvin.erik.mobibot.modules
-/**
- * The `ModuleException` class.
- */
class ModuleException @JvmOverloads constructor(
val debugMessage: String,
message: String? = null,
cause: Throwable? = null
) : Exception(message, cause) {
companion object {
- @Suppress("ConstPropertyName")
+ @Suppress("unused")
private const val serialVersionUID = 1L
}
}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/modules/Ping.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/Ping.kt
index ca18216..4451e99 100644
--- a/src/main/kotlin/net/thauvin/erik/mobibot/modules/Ping.kt
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/Ping.kt
@@ -35,15 +35,11 @@ import net.thauvin.erik.mobibot.Utils.helpFormat
import org.pircbotx.hooks.types.GenericMessageEvent
/**
- * The Ping module.
+ * Responds with a random quirky response.
*/
class Ping : AbstractModule() {
override val name = "Ping"
- override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
- event.bot().sendIRC().action(channel, randomPing())
- }
-
companion object {
/**
* The ping responses.
@@ -80,4 +76,8 @@ class Ping : AbstractModule() {
help.add("To ping the bot:")
help.add(helpFormat("%c $PING_CMD"))
}
+
+ override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
+ event.bot().sendIRC().action(channel, randomPing())
+ }
}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/modules/RockPaperScissors.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/RockPaperScissors.kt
index b8c81f1..75d781c 100644
--- a/src/main/kotlin/net/thauvin/erik/mobibot/modules/RockPaperScissors.kt
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/RockPaperScissors.kt
@@ -37,11 +37,25 @@ import org.pircbotx.hooks.types.GenericMessageEvent
/**
- * Simple module example in Kotlin.
+ * Allows users to play Rock Paper Scissors.
*/
class RockPaperScissors : AbstractModule() {
override val name = "RockPaperScissors"
+ companion object {
+ // For testing.
+ fun winLoseOrDraw(player: String, bot: String): String {
+ val hand = Hands.valueOf(player.uppercase())
+ val botHand = Hands.valueOf(bot.uppercase())
+
+ return when {
+ hand == botHand -> "draw"
+ hand.beats(botHand) -> "win"
+ else -> "lose"
+ }
+ }
+ }
+
init {
with(commands) {
add(Hands.ROCK.name.lowercase())
@@ -80,23 +94,9 @@ class RockPaperScissors : AbstractModule() {
abstract fun beats(hand: Hands): Boolean
}
- companion object {
- // For testing.
- fun winLoseOrDraw(player: String, bot: String): String {
- val hand = Hands.valueOf(player.uppercase())
- val botHand = Hands.valueOf(bot.uppercase())
-
- return when {
- hand == botHand -> "draw"
- hand.beats(botHand) -> "win"
- else -> "lose"
- }
- }
- }
-
override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
val hand = Hands.valueOf(cmd.uppercase())
- val botHand = Hands.entries[(0..Hands.entries.size).random()]
+ val botHand = Hands.entries[(0..Hands.entries.size - 1).random()]
when {
hand == botHand -> {
event.respond("${hand.name} vs. ${botHand.name} » You ${"tie".bold()}.")
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/modules/StockQuote.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/StockQuote.kt
index d71c91a..d267a30 100644
--- a/src/main/kotlin/net/thauvin/erik/mobibot/modules/StockQuote.kt
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/StockQuote.kt
@@ -30,7 +30,6 @@
*/
package net.thauvin.erik.mobibot.modules
-import net.thauvin.erik.mobibot.Utils.capitalise
import net.thauvin.erik.mobibot.Utils.encodeUrl
import net.thauvin.erik.mobibot.Utils.helpFormat
import net.thauvin.erik.mobibot.Utils.reader
@@ -49,33 +48,12 @@ import java.io.IOException
import java.net.URL
/**
- * The StockQuote module.
+ * Retrieves stock quotes from Alpha Vantage.
*/
class StockQuote : AbstractModule() {
private val logger: Logger = LoggerFactory.getLogger(StockQuote::class.java)
- override val name = "StockQuote"
-
- /**
- * Returns the specified stock quote from Alpha Vantage.
- */
- override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
- if (args.isNotBlank()) {
- try {
- val messages = getQuote(args, properties[API_KEY_PROP])
- for (msg in messages) {
- event.sendMessage(channel, msg)
- }
- } catch (e: ModuleException) {
- if (logger.isWarnEnabled) logger.warn(e.debugMessage, e)
- e.message?.let {
- event.respond(it)
- }
- }
- } else {
- helpResponse(event)
- }
- }
+ override val name = SERVICE_NAME
companion object {
/**
@@ -88,6 +66,11 @@ class StockQuote : AbstractModule() {
*/
const val INVALID_SYMBOL = "Invalid symbol."
+ /**
+ * The service name.
+ */
+ const val SERVICE_NAME = "StockQuote"
+
// API URL
private const val API_URL = "https://www.alphavantage.co/query?function="
@@ -103,7 +86,7 @@ class StockQuote : AbstractModule() {
if (info.isNotEmpty()) {
throw ModuleException(debugMessage, info.unescapeXml())
}
- } catch (ignore: JSONException) {
+ } catch (_: JSONException) {
// Do nothing
}
try {
@@ -115,7 +98,7 @@ class StockQuote : AbstractModule() {
if (error.isNotEmpty()) {
throw ModuleException(debugMessage, error.unescapeXml())
}
- } catch (ignore: JSONException) {
+ } catch (_: JSONException) {
// Do nothing
}
json
@@ -132,8 +115,8 @@ class StockQuote : AbstractModule() {
fun getQuote(symbol: String, apiKey: String?): List {
if (apiKey.isNullOrBlank()) {
throw ModuleException(
- "${StockQuote::class.java.name} is disabled.",
- "${STOCK_CMD.capitalise()} is disabled. The API key is missing."
+ "$SERVICE_NAME is disabled.",
+ "$SERVICE_NAME is disabled. The API key is missing."
)
}
val messages = mutableListOf()
@@ -233,4 +216,25 @@ class StockQuote : AbstractModule() {
help.add(helpFormat("%c $STOCK_CMD "))
initProperties(API_KEY_PROP)
}
+
+ /**
+ * Returns the specified stock quote from Alpha Vantage.
+ */
+ override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
+ if (args.isNotBlank()) {
+ try {
+ val messages = getQuote(args, properties[API_KEY_PROP])
+ for (msg in messages) {
+ event.sendMessage(channel, msg)
+ }
+ } catch (e: ModuleException) {
+ if (logger.isWarnEnabled) logger.warn(e.debugMessage, e)
+ e.message?.let {
+ event.respond(it)
+ }
+ }
+ } else {
+ helpResponse(event)
+ }
+ }
}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/modules/War.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/War.kt
index 70ac4ec..59b5a8e 100644
--- a/src/main/kotlin/net/thauvin/erik/mobibot/modules/War.kt
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/War.kt
@@ -36,14 +36,31 @@ import org.pircbotx.hooks.types.GenericMessageEvent
import java.security.SecureRandom
/**
- * The War module.
- *
- * @author [Erik C. Thauvin](https://erik.thauvin.net/)
- * @since 1.0
+ * Plays the `war` card game.
*/
class War : AbstractModule() {
override val name = "War"
+ companion object {
+ private val CLUBS = arrayOf("🃑", "🃞", "🃝", "🃛", "🃚", "🃙", "🃘", "🃗", "🃖", "🃕", "🃔", "🃓", "🃒")
+ private val DIAMONDS = arrayOf("🃁", "🃎", "🃍", "🃋", "🃊", "🃉", "🃈", "🃇", "🃆", "🃅", "🃄", "🃃", "🃂")
+ private val HEARTS = arrayOf("🂱", "🂾", "🂽", "🂻", "🂺", "🂹", "🂸", "🂷", "🂶", "🂵", "🂴", "🂳", "🂲")
+
+ // Random
+ private val RANDOM = SecureRandom()
+ private val SPADES = arrayOf("🂡", "🂮", "🂭", "🂫", "🂪", "🂩", "🂨", "🂧", "🂦", "🂥", "🂤", "🂣", "🂢")
+ private val DECK = arrayOf(HEARTS, SPADES, DIAMONDS, CLUBS)
+
+ // War command
+ private const val WAR_CMD = "war"
+ }
+
+ init {
+ commands.add(WAR_CMD)
+ help.add("To play war:")
+ help.add(helpFormat("%c $WAR_CMD"))
+ }
+
override fun commandResponse(
channel: String, cmd: String, args: String,
event: GenericMessageEvent
@@ -66,24 +83,4 @@ class War : AbstractModule() {
)
} while (i == y)
}
-
- companion object {
- private val CLUBS = arrayOf("🃑", "🃞", "🃝", "🃛", "🃚", "🃙", "🃘", "🃗", "🃖", "🃕", "🃔", "🃓", "🃒")
- private val DIAMONDS = arrayOf("🃁", "🃎", "🃍", "🃋", "🃊", "🃉", "🃈", "🃇", "🃆", "🃅", "🃄", "🃃", "🃂")
- private val HEARTS = arrayOf("🂱", "🂾", "🂽", "🂻", "🂺", "🂹", "🂸", "🂷", "🂶", "🂵", "🂴", "🂳", "🂲")
-
- // Random
- private val RANDOM = SecureRandom()
- private val SPADES = arrayOf("🂡", "🂮", "🂭", "🂫", "🂪", "🂩", "🂨", "🂧", "🂦", "🂥", "🂤", "🂣", "🂢")
- private val DECK = arrayOf(HEARTS, SPADES, DIAMONDS, CLUBS)
-
- // War command
- private const val WAR_CMD = "war"
- }
-
- init {
- commands.add(WAR_CMD)
- help.add("To play war:")
- help.add(helpFormat("%c $WAR_CMD"))
- }
}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/modules/Weather2.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/Weather2.kt
index 074edd0..224c377 100644
--- a/src/main/kotlin/net/thauvin/erik/mobibot/modules/Weather2.kt
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/Weather2.kt
@@ -35,7 +35,7 @@ import net.aksingh.owmjapis.core.OWM
import net.aksingh.owmjapis.core.OWM.Country
import net.aksingh.owmjapis.model.CurrentWeather
import net.thauvin.erik.mobibot.Utils.bold
-import net.thauvin.erik.mobibot.Utils.capitalise
+import net.thauvin.erik.mobibot.Utils.capitalize
import net.thauvin.erik.mobibot.Utils.capitalizeWords
import net.thauvin.erik.mobibot.Utils.encodeUrl
import net.thauvin.erik.mobibot.Utils.helpFormat
@@ -51,37 +51,12 @@ import org.slf4j.LoggerFactory
import kotlin.math.roundToInt
/**
- * The `Weather2` module.
+ * Retrieve weather information from OpenWeatherMap.
*/
class Weather2 : AbstractModule() {
private val logger: Logger = LoggerFactory.getLogger(Weather2::class.java)
- override val name = "Weather"
-
- /**
- * Fetches the weather data from a specific city.
- */
- override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
- if (args.isNotBlank()) {
- try {
- val messages = getWeather(args, properties[API_KEY_PROP])
- if (messages[0].isError) {
- helpResponse(event)
- } else {
- for (msg in messages) {
- event.sendMessage(channel, msg)
- }
- }
- } catch (e: ModuleException) {
- if (logger.isWarnEnabled) logger.warn(e.debugMessage, e)
- e.message?.let {
- event.respond(it)
- }
- }
- } else {
- helpResponse(event)
- }
- }
+ override val name = WEATHER_NAME
companion object {
/**
@@ -89,6 +64,11 @@ class Weather2 : AbstractModule() {
*/
const val API_KEY_PROP = "owm-api-key"
+ /**
+ * The service name.
+ */
+ const val WEATHER_NAME = "Weather"
+
// Weather command
private const val WEATHER_CMD = "weather"
@@ -121,7 +101,7 @@ class Weather2 : AbstractModule() {
if (apiKey.isNullOrBlank()) {
throw ModuleException(
"${Weather2::class.java.name} is disabled.",
- "${WEATHER_CMD.capitalise()} is disabled. The API key is missing."
+ "${WEATHER_CMD.capitalize()} is disabled. The API key is missing."
)
}
val owm = OWM(apiKey)
@@ -181,7 +161,7 @@ class Weather2 : AbstractModule() {
for (w in it) {
w?.let {
condition.append(' ')
- .append(w.getDescription().capitalise())
+ .append(w.getDescription().capitalize())
.append('.')
}
}
@@ -247,4 +227,29 @@ class Weather2 : AbstractModule() {
}
initProperties(API_KEY_PROP)
}
+
+ /**
+ * Fetches the weather data from a specific location.
+ */
+ override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
+ if (args.isNotBlank()) {
+ try {
+ val messages = getWeather(args, properties[API_KEY_PROP])
+ if (messages[0].isError) {
+ helpResponse(event)
+ } else {
+ for (msg in messages) {
+ event.sendMessage(channel, msg)
+ }
+ }
+ } catch (e: ModuleException) {
+ if (logger.isWarnEnabled) logger.warn(e.debugMessage, e)
+ e.message?.let {
+ event.respond(it)
+ }
+ }
+ } else {
+ helpResponse(event)
+ }
+ }
}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/modules/WolframAlpha.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/WolframAlpha.kt
index 2e2e7ec..6d89bda 100644
--- a/src/main/kotlin/net/thauvin/erik/mobibot/modules/WolframAlpha.kt
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/WolframAlpha.kt
@@ -42,10 +42,84 @@ import org.slf4j.LoggerFactory
import java.io.IOException
import java.net.URL
+/**
+ * Allows user to query Wolfram Alpha.
+ */
class WolframAlpha : AbstractModule() {
private val logger: Logger = LoggerFactory.getLogger(WolframAlpha::class.java)
- override val name = "WolframAlpha"
+ override val name = SERVICE_NAME
+
+ companion object {
+ /**
+ * The Wolfram Alpha AppID property.
+ */
+ const val APPID_KEY_PROP = "wolfram-appid"
+
+ /**
+ * Metric unit
+ */
+ const val METRIC = "metric"
+
+ /**
+ * Imperial unit
+ */
+ const val IMPERIAL = "imperial"
+
+ /**
+ * The service name.
+ */
+ const val SERVICE_NAME = "WolframAlpha"
+
+ /**
+ * The Wolfram units properties
+ */
+ const val UNITS_PROP = "wolfram-units"
+
+ // Wolfram command
+ private const val WOLFRAM_CMD = "wolfram"
+
+ // Wolfram Alpha API URL
+ private const val API_URL = "https://api.wolframalpha.com/v1/spoken?appid="
+
+ @JvmStatic
+ @Throws(ModuleException::class)
+ fun queryWolfram(query: String, units: String = IMPERIAL, appId: String?): String {
+ if (!appId.isNullOrEmpty()) {
+ try {
+ val urlReader = URL("${API_URL}${appId}&units=${units}&i=" + query.encodeUrl()).reader()
+ if (urlReader.responseCode.isHttpSuccess()) {
+ return urlReader.body
+ } else {
+ throw ModuleException(
+ "wolfram($query): ${urlReader.responseCode} : ${urlReader.body} ",
+ urlReader.body.ifEmpty {
+ "Looks like $SERVICE_NAME isn't able to answer that. (${urlReader.responseCode})"
+ }
+ )
+ }
+ } catch (ioe: IOException) {
+ throw ModuleException(
+ "wolfram($query): IOE", "An IO Error occurred while querying $SERVICE_NAME.", ioe
+ )
+ }
+ } else {
+ throw ModuleException("wolfram($query): No API Key", "No $SERVICE_NAME AppID specified.")
+ }
+ }
+ }
+
+ init {
+ commands.add(WOLFRAM_CMD)
+ with(help) {
+ add("To get answers from Wolfram Alpha:")
+ add(Utils.helpFormat("%c $WOLFRAM_CMD [units=(${METRIC}|${IMPERIAL})]"))
+ add("For example:")
+ add(Utils.helpFormat("%c $WOLFRAM_CMD days until christmas"))
+ add(Utils.helpFormat("%c $WOLFRAM_CMD distance earth moon units=metric"))
+ }
+ initProperties(APPID_KEY_PROP, UNITS_PROP)
+ }
private fun getUnits(unit: String?): String {
return if (unit?.lowercase() == METRIC) {
@@ -55,6 +129,9 @@ class WolframAlpha : AbstractModule() {
}
}
+ /**
+ * Queries Wolfram Alpha.
+ */
override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
if (args.isNotBlank()) {
try {
@@ -80,63 +157,4 @@ class WolframAlpha : AbstractModule() {
helpResponse(event)
}
}
-
- companion object {
- /**
- * The Wolfram Alpha API Key property.
- */
- const val APPID_KEY_PROP = "wolfram-appid"
-
- /**
- * The Wolfram units properties
- */
- const val UNITS_PROP = "wolfram-units"
-
- const val METRIC = "metric"
- const val IMPERIAL = "imperial"
-
- // Wolfram command
- private const val WOLFRAM_CMD = "wolfram"
-
- // Wolfram Alpha API URL
- private const val API_URL = "http://api.wolframalpha.com/v1/spoken?appid="
-
- @JvmStatic
- @Throws(ModuleException::class)
- fun queryWolfram(query: String, units: String = IMPERIAL, appId: String?): String {
- if (!appId.isNullOrEmpty()) {
- try {
- val urlReader = URL("${API_URL}${appId}&units=${units}&i=" + query.encodeUrl()).reader()
- if (urlReader.responseCode.isHttpSuccess()) {
- return urlReader.body
- } else {
- throw ModuleException(
- "wolfram($query): ${urlReader.responseCode} : ${urlReader.body} ",
- urlReader.body.ifEmpty {
- "Looks like Wolfram Alpha isn't able to answer that. (${urlReader.responseCode})"
- }
- )
- }
- } catch (ioe: IOException) {
- throw ModuleException(
- "wolfram($query): IOE", "An IO Error occurred while querying Wolfram Alpha.", ioe
- )
- }
- } else {
- throw ModuleException("wolfram($query): No API Key", "No Wolfram Alpha API key specified.")
- }
- }
- }
-
- init {
- commands.add(WOLFRAM_CMD)
- with(help) {
- add("To get answers from Wolfram Alpha:")
- add(Utils.helpFormat("%c $WOLFRAM_CMD [units=(${METRIC}|${IMPERIAL})]"))
- add("For example:")
- add(Utils.helpFormat("%c $WOLFRAM_CMD days until christmas"))
- add(Utils.helpFormat("%c $WOLFRAM_CMD distance earth moon units=metric"))
- }
- initProperties(APPID_KEY_PROP, UNITS_PROP)
- }
}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/modules/WorldTime.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/WorldTime.kt
index afc0a5f..3c73703 100644
--- a/src/main/kotlin/net/thauvin/erik/mobibot/modules/WorldTime.kt
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/WorldTime.kt
@@ -328,7 +328,7 @@ class WorldTime : AbstractModule() {
// The Time command
private const val TIME_CMD = "time"
- // The zones arguments
+ // The `zones` arguments
private const val ZONES_ARGS = "zones"
// The default zone
@@ -367,6 +367,16 @@ class WorldTime : AbstractModule() {
}
}
+ init {
+ with(help) {
+ add("To display a country's current date/time:")
+ add(helpFormat("%c $TIME_CMD []"))
+ add("For a listing of the supported countries/zones:")
+ add(helpFormat("%c $TIME_CMD $ZONES_ARGS"))
+ }
+ commands.add(TIME_CMD)
+ }
+
override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
if (args.equals(ZONES_ARGS, true)) {
event.sendMessage("The supported countries/zones are: ")
@@ -377,14 +387,4 @@ class WorldTime : AbstractModule() {
}
override val isPrivateMsgEnabled = true
-
- init {
- with(help) {
- add("To display a country's current date/time:")
- add(helpFormat("%c $TIME_CMD []"))
- add("For a listing of the supported countries/zones:")
- add(helpFormat("%c $TIME_CMD $ZONES_ARGS"))
- }
- commands.add(TIME_CMD)
- }
}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/msg/ErrorMessage.kt b/src/main/kotlin/net/thauvin/erik/mobibot/msg/ErrorMessage.kt
index 56e7b92..03bea5c 100644
--- a/src/main/kotlin/net/thauvin/erik/mobibot/msg/ErrorMessage.kt
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/msg/ErrorMessage.kt
@@ -31,7 +31,7 @@
package net.thauvin.erik.mobibot.msg
/**
- * The `ErrorMessage` class.
+ * Holds an error message.
*/
class ErrorMessage @JvmOverloads constructor(msg: String, color: String = DEFAULT_COLOR) :
Message(msg, color, isError = true)
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/msg/Message.kt b/src/main/kotlin/net/thauvin/erik/mobibot/msg/Message.kt
index 1a6e58b..71a76b4 100644
--- a/src/main/kotlin/net/thauvin/erik/mobibot/msg/Message.kt
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/msg/Message.kt
@@ -30,8 +30,9 @@
*/
package net.thauvin.erik.mobibot.msg
+
/**
- * The `Message` class.
+ * Holds a message.
*/
open class Message @JvmOverloads constructor(
var msg: String,
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/msg/NoticeMessage.kt b/src/main/kotlin/net/thauvin/erik/mobibot/msg/NoticeMessage.kt
index f06ce89..e5af8b4 100644
--- a/src/main/kotlin/net/thauvin/erik/mobibot/msg/NoticeMessage.kt
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/msg/NoticeMessage.kt
@@ -31,7 +31,7 @@
package net.thauvin.erik.mobibot.msg
/**
- * The `NoticeMessage` class.
+ * Holds a notice message.
*/
class NoticeMessage @JvmOverloads constructor(msg: String, color: String = DEFAULT_COLOR) :
Message(msg, color, isNotice = true)
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/msg/PrivateMessage.kt b/src/main/kotlin/net/thauvin/erik/mobibot/msg/PrivateMessage.kt
index ef0eeb1..c189142 100644
--- a/src/main/kotlin/net/thauvin/erik/mobibot/msg/PrivateMessage.kt
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/msg/PrivateMessage.kt
@@ -30,8 +30,9 @@
*/
package net.thauvin.erik.mobibot.msg
+
/**
- * The `PrivateMessage` class.
+ * Holds a private message.
*/
class PrivateMessage @JvmOverloads constructor(msg: String, color: String = DEFAULT_COLOR) :
Message(msg, color, isPrivate = true)
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/msg/PublicMessage.kt b/src/main/kotlin/net/thauvin/erik/mobibot/msg/PublicMessage.kt
index be6583f..42da53d 100644
--- a/src/main/kotlin/net/thauvin/erik/mobibot/msg/PublicMessage.kt
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/msg/PublicMessage.kt
@@ -31,6 +31,6 @@
package net.thauvin.erik.mobibot.msg
/**
- * The `PublicMessage` class.
+ * Holds a public message.
*/
class PublicMessage @JvmOverloads constructor(msg: String, color: String = DEFAULT_COLOR) : Message(msg, color)
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/social/SocialManager.kt b/src/main/kotlin/net/thauvin/erik/mobibot/social/SocialManager.kt
index de9653d..98df4f2 100644
--- a/src/main/kotlin/net/thauvin/erik/mobibot/social/SocialManager.kt
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/social/SocialManager.kt
@@ -39,7 +39,7 @@ import org.slf4j.LoggerFactory
import java.util.*
/**
- * Social Manager.
+ * Manages social media modules and handles operations such as notifications, posting, and queuing entries.
*/
class SocialManager {
private val entries: MutableSet = HashSet()
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/social/SocialModule.kt b/src/main/kotlin/net/thauvin/erik/mobibot/social/SocialModule.kt
index d45cf5c..1950916 100644
--- a/src/main/kotlin/net/thauvin/erik/mobibot/social/SocialModule.kt
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/social/SocialModule.kt
@@ -40,6 +40,9 @@ import org.pircbotx.hooks.types.GenericMessageEvent
import org.slf4j.Logger
import org.slf4j.LoggerFactory
+/**
+ * Provides the ability to handle notifications, entries and manage interaction with a specific social media service.
+ */
abstract class SocialModule : AbstractModule() {
private val logger: Logger = LoggerFactory.getLogger(SocialManager::class.java)
@@ -65,7 +68,7 @@ abstract class SocialModule : AbstractModule() {
abstract fun post(message: String, isDm: Boolean): String
/**
- * Post entry to social media.
+ * Post an entry to social media.
*/
fun postEntry(index: Int) {
if (isAutoPost && LinksManager.entries.links.size >= index) {
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/social/SocialTimer.kt b/src/main/kotlin/net/thauvin/erik/mobibot/social/SocialTimer.kt
index aadebf5..b5316c0 100644
--- a/src/main/kotlin/net/thauvin/erik/mobibot/social/SocialTimer.kt
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/social/SocialTimer.kt
@@ -33,6 +33,9 @@ package net.thauvin.erik.mobibot.social
import java.util.*
+/**
+ * Timer used to post social entries.
+ */
class SocialTimer(private var socialManager: SocialManager, private var index: Int) : TimerTask() {
override fun run() {
socialManager.postEntry(index)
diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/AddonsTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/AddonsTest.kt
index 27163fb..5b7995f 100644
--- a/src/test/kotlin/net/thauvin/erik/mobibot/AddonsTest.kt
+++ b/src/test/kotlin/net/thauvin/erik/mobibot/AddonsTest.kt
@@ -53,7 +53,7 @@ class AddonsTest {
private val addons = Addons(p)
@Test
- fun addTest() {
+ fun `Add modules and commands`() {
// Modules
addons.add(Joke())
addons.add(RockPaperScissors())
diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/DisableOnCi.kt b/src/test/kotlin/net/thauvin/erik/mobibot/DisableOnCi.kt
index 75a1cf9..9e6fd8b 100644
--- a/src/test/kotlin/net/thauvin/erik/mobibot/DisableOnCi.kt
+++ b/src/test/kotlin/net/thauvin/erik/mobibot/DisableOnCi.kt
@@ -34,9 +34,6 @@ import org.junit.jupiter.api.extension.ExtendWith
/**
* Disables tests on CI annotation.
- *
- * @author [Erik C. Thauvin](https://erik.thauvin.net/)
- * @since 1.0
*/
@Target(AnnotationTarget.TYPE, AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/DisableOnCiCondition.kt b/src/test/kotlin/net/thauvin/erik/mobibot/DisableOnCiCondition.kt
index d887b55..165b257 100644
--- a/src/test/kotlin/net/thauvin/erik/mobibot/DisableOnCiCondition.kt
+++ b/src/test/kotlin/net/thauvin/erik/mobibot/DisableOnCiCondition.kt
@@ -36,9 +36,6 @@ import org.junit.jupiter.api.extension.ExtensionContext
/**
* Disables tests on CI condition.
- *
- * @author [Erik C. Thauvin](https://erik.thauvin.net/)
- * @since 1.0
*/
class DisableOnCiCondition : ExecutionCondition {
override fun evaluateExecutionCondition(context: ExtensionContext): ConditionEvaluationResult {
diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/FeedReaderTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/FeedReaderTest.kt
index 0c3d1c6..1c01b1c 100644
--- a/src/test/kotlin/net/thauvin/erik/mobibot/FeedReaderTest.kt
+++ b/src/test/kotlin/net/thauvin/erik/mobibot/FeedReaderTest.kt
@@ -37,39 +37,68 @@ import assertk.assertions.*
import com.rometools.rome.io.FeedException
import net.thauvin.erik.mobibot.FeedReader.Companion.readFeed
import net.thauvin.erik.mobibot.msg.Message
+import org.junit.jupiter.api.DisplayName
+import org.junit.jupiter.api.Nested
import java.io.IOException
import java.net.MalformedURLException
import java.net.UnknownHostException
import kotlin.test.Test
class FeedReaderTest {
- @Test
- fun readFeedTest() {
- var messages = readFeed("https://feeds.thauvin.net/ethauvin")
- assertThat(messages, "messages").all {
- size().isEqualTo(10)
- index(1).prop(Message::msg).contains("erik.thauvin.net")
+ @Nested
+ @DisplayName("Failure Tests")
+ inner class FailureTests {
+ @Test
+ fun invalidFeed() {
+ assertFailure { readFeed("https://www.example.com") }.isInstanceOf(FeedException::class.java)
}
- messages = readFeed("https://lorem-rss.herokuapp.com/feed?length=0")
- assertThat(messages, "messages").index(0).prop(Message::msg).contains("nothing")
+ @Test
+ fun invalidHost() {
+ assertFailure { readFeed("https://www.examplesfoo.com/") }.isInstanceOf(UnknownHostException::class.java)
+ }
- messages = readFeed("https://lorem-rss.herokuapp.com/feed?length=84", 42)
- assertThat(messages, "messages").size().isEqualTo(84)
- messages.forEachIndexed { i, m ->
- if (i % 2 == 0) {
- assertThat(m, "messages($i)").prop(Message::msg).startsWith("Lorem ipsum")
- } else {
- assertThat(m, "messages($i)").prop(Message::msg).contains("http://example.com/test/")
+ @Test
+ fun invalidLocation() {
+ assertFailure { readFeed("https://www.thauvin.net/foo") }.isInstanceOf(IOException::class.java)
+ }
+
+ @Test
+ fun invalidUrl() {
+ assertFailure { readFeed("blah") }.isInstanceOf(MalformedURLException::class.java)
+ }
+ }
+
+ @Nested
+ @DisplayName("Read Feed Tests")
+ inner class ReadFeedTests {
+ @Test
+ fun readEmptyFeed() {
+ val messages = readFeed("https://lorem-rss.herokuapp.com/feed?length=0")
+ assertThat(messages, "messages").index(0).prop(Message::msg).contains("nothing")
+ }
+
+ @Test
+ fun readFeed() {
+ val messages = readFeed("https://feeds.thauvin.net/ethauvin")
+ assertThat(messages, "messages").all {
+ size().isEqualTo(10)
+ index(1).prop(Message::msg).contains("erik.thauvin.net")
}
}
- assertFailure { readFeed("blah") }.isInstanceOf(MalformedURLException::class.java)
-
- assertFailure { readFeed("https://www.example.com") }.isInstanceOf(FeedException::class.java)
-
- assertFailure { readFeed("https://www.thauvin.net/foo") }.isInstanceOf(IOException::class.java)
-
- assertFailure { readFeed("https://www.examplesfoo.com/") }.isInstanceOf(UnknownHostException::class.java)
+ @Test
+ fun readThenValidateFeedContent() {
+ val messages = readFeed("https://lorem-rss.herokuapp.com/feed?length=84", 42)
+ assertThat(messages, "messages").size().isEqualTo(84)
+ messages.forEachIndexed { i, m ->
+ if (i % 2 == 0) {
+ assertThat(m, "messages($i)").prop(Message::msg).startsWith("Lorem ipsum")
+ } else {
+ @Suppress("HttpUrlsUsage")
+ assertThat(m, "messages($i)").prop(Message::msg).contains("http://example.com/test/")
+ }
+ }
+ }
}
}
diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/LocalProperties.kt b/src/test/kotlin/net/thauvin/erik/mobibot/LocalProperties.kt
index 646a0ea..199ca1d 100644
--- a/src/test/kotlin/net/thauvin/erik/mobibot/LocalProperties.kt
+++ b/src/test/kotlin/net/thauvin/erik/mobibot/LocalProperties.kt
@@ -38,7 +38,7 @@ import java.nio.file.Paths
import java.util.*
/**
- * Access to `local.properties`.
+ * Provides functions to access local properties and environment variables.
*/
open class LocalProperties {
init {
@@ -72,7 +72,7 @@ open class LocalProperties {
env?.let {
localProps.setProperty(key, env)
}
- env
+ throw IOException("The $key property not found in local.properties or environment variables.")
}
}
diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/PinboardTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/PinboardTest.kt
index dafb862..306b92b 100644
--- a/src/test/kotlin/net/thauvin/erik/mobibot/PinboardTest.kt
+++ b/src/test/kotlin/net/thauvin/erik/mobibot/PinboardTest.kt
@@ -40,35 +40,26 @@ import kotlin.test.assertFalse
import kotlin.test.assertTrue
class PinboardTest : LocalProperties() {
- private val pinboard = Pinboard()
+ private val apiToken = getProperty("pinboard-api-token")
- @Test
- fun testPinboard() {
- val apiToken = getProperty("pinboard-api-token")
- val url = "https://www.example.com/${(1000..5000).random()}"
- val ircServer = "irc.test.com"
- val entry = EntryLink(url, "Test Example", "ErikT", "", "#mobitopia", listOf("test"))
+ private val ircServer = "irc.test.com"
+ private val pinboard = Pinboard().apply { setApiToken(apiToken) }
- pinboard.setApiToken(apiToken)
+ private fun newEntry(): EntryLink {
+ return EntryLink(
+ randomUrl(), "Test Example", "ErikT", "", "#mobitopia", listOf("test")
+ )
+ }
- pinboard.addPin(ircServer, entry)
- assertTrue(validatePin(apiToken, url = entry.link, entry.title, entry.nick, entry.channel), "addPin")
-
- entry.link = "https://www.example.com/${(5001..9999).random()}"
- pinboard.updatePin(ircServer, url, entry)
- assertTrue(validatePin(apiToken, url = entry.link, ircServer), "updatePin")
-
- entry.title = "Foo Title"
- pinboard.updatePin(ircServer, entry.link, entry)
- assertTrue(validatePin(apiToken, url = entry.link, entry.title), "updatePin(${entry.title}")
-
- pinboard.deletePin(entry)
- assertFalse(validatePin(apiToken, url = entry.link), "deletePin")
+ private fun randomUrl(): String {
+ return "https://www.example.com/${(5001..9999).random()}"
}
private fun validatePin(apiToken: String, url: String, vararg matches: String): Boolean {
val response =
- URL("https://api.pinboard.in/v1/posts/get?auth_token=${apiToken}&tag=test&" + url.encodeUrl()).reader().body
+ URL(
+ "https://api.pinboard.in/v1/posts/get?auth_token=${apiToken}&tag=test&" + url.encodeUrl()
+ ).reader().body
matches.forEach {
if (!response.contains(it)) {
@@ -78,4 +69,43 @@ class PinboardTest : LocalProperties() {
return response.contains(url)
}
+
+ @Test
+ fun addPin() {
+ val entry = newEntry()
+ pinboard.addPin(ircServer, entry)
+ assertTrue(validatePin(apiToken, url = entry.link, entry.title, entry.nick, entry.channel), "addPin")
+
+ pinboard.deletePin(entry)
+ assertFalse(validatePin(apiToken, url = entry.link, entry.title, entry.nick, entry.channel), "deletePin")
+
+ }
+
+ @Test
+ fun updatePin() {
+ val entry = newEntry()
+ pinboard.addPin(ircServer, entry)
+ assertTrue(validatePin(apiToken, url = entry.link, entry.title, entry.nick, entry.channel), "addPin")
+
+ val url = entry.link
+ entry.link = randomUrl()
+ pinboard.updatePin(ircServer, url, entry)
+ assertTrue(validatePin(apiToken, url = entry.link, ircServer), "updatePin")
+
+ pinboard.deletePin(entry)
+ assertFalse(validatePin(apiToken, url = entry.link, entry.title, entry.nick, entry.channel), "deletePin")
+ }
+
+ @Test
+ fun updatePinTitle() {
+ val entry = newEntry()
+ pinboard.addPin(ircServer, entry)
+ assertTrue(validatePin(apiToken, url = entry.link, entry.title, entry.nick, entry.channel), "addPin")
+
+ pinboard.updatePin(ircServer, entry.link, entry)
+ assertTrue(validatePin(apiToken, url = entry.link, entry.title), "updatePin")
+
+ pinboard.deletePin(entry)
+ assertFalse(validatePin(apiToken, url = entry.link, entry.title, entry.nick, entry.channel), "deletePin")
+ }
}
diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/UtilsTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/UtilsTest.kt
index bd05f70..3777cb1 100644
--- a/src/test/kotlin/net/thauvin/erik/mobibot/UtilsTest.kt
+++ b/src/test/kotlin/net/thauvin/erik/mobibot/UtilsTest.kt
@@ -36,7 +36,7 @@ import assertk.assertions.isEqualTo
import assertk.assertions.length
import net.thauvin.erik.mobibot.Utils.appendIfMissing
import net.thauvin.erik.mobibot.Utils.bold
-import net.thauvin.erik.mobibot.Utils.capitalise
+import net.thauvin.erik.mobibot.Utils.capitalize
import net.thauvin.erik.mobibot.Utils.capitalizeWords
import net.thauvin.erik.mobibot.Utils.colorize
import net.thauvin.erik.mobibot.Utils.cyan
@@ -60,6 +60,8 @@ import net.thauvin.erik.mobibot.Utils.underline
import net.thauvin.erik.mobibot.Utils.unescapeXml
import net.thauvin.erik.mobibot.msg.Message.Companion.DEFAULT_COLOR
import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.DisplayName
+import org.junit.jupiter.api.Nested
import org.pircbotx.Colors
import java.io.File
import java.io.IOException
@@ -71,205 +73,361 @@ import kotlin.test.Test
class UtilsTest {
private val ascii =
" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"
- private val cal = Calendar.getInstance()
- private val localDateTime = LocalDateTime.of(1952, 2, 17, 12, 30, 0)
+ private val p = Properties().apply {
+ setProperty("one", "1")
+ setProperty("two", "two")
+ }
private val test = "This is a test."
- @BeforeEach
- fun setUp() {
- cal[1952, Calendar.FEBRUARY, 17, 12, 30] = 0
- }
+ @Nested
+ @DisplayName("Date Tests")
+ inner class DateTests {
+ private val cal = Calendar.getInstance()
+ private val localDateTime = LocalDateTime.of(1952, 2, 17, 12, 30, 0)
- @Test
- fun testAppendIfMissing() {
- val dir = "dir"
- val sep = '/'
- val url = "https://erik.thauvin.net"
- assertThat(dir.appendIfMissing(File.separatorChar), "appendIfMissing(dir)")
- .isEqualTo(dir + File.separatorChar)
- assertThat(url.appendIfMissing(sep), "appendIfMissing(url)").isEqualTo("$url$sep")
- assertThat("$url$sep".appendIfMissing(sep), "appendIfMissing($url$sep)").isEqualTo("$url$sep")
- }
-
- @Test
- fun testBold() {
- assertThat(1.bold(), "bold(1)").isEqualTo(Colors.BOLD + "1" + Colors.BOLD)
- assertThat(2L.bold(), "bold(2L)").isEqualTo(Colors.BOLD + "2" + Colors.BOLD)
- assertThat(ascii.bold(), "ascii.bold()").isEqualTo(Colors.BOLD + ascii + Colors.BOLD)
- assertThat("test".bold(), "test.bold()").isEqualTo(Colors.BOLD + "test" + Colors.BOLD)
- }
-
-
- @Test
- fun testCapitalise() {
- assertThat("test".capitalise(), "capitalize(test)").isEqualTo("Test")
- assertThat("Test".capitalise(), "capitalize(Test)").isEqualTo("Test")
- assertThat(test.capitalise(), "capitalize($test)").isEqualTo(test)
- assertThat("".capitalise(), "capitalize()").isEqualTo("")
- }
-
- @Test
- fun textCapitaliseWords() {
- assertThat(test.capitalizeWords(), "captiatlizeWords(test)").isEqualTo("This Is A Test.")
- assertThat("Already Capitalized".capitalizeWords(), "already capitalized")
- .isEqualTo("Already Capitalized")
- assertThat(" a test ".capitalizeWords(), "with spaces").isEqualTo(" A Test ")
- }
-
- @Test
- fun testColorize() {
- assertThat(ascii.colorize(Colors.REVERSE), "reverse.colorize()").isEqualTo(
- Colors.REVERSE + ascii + Colors.REVERSE
- )
- assertThat(ascii.colorize(Colors.RED), "red.colorize()")
- .isEqualTo(Colors.RED + ascii + Colors.NORMAL)
- assertThat(ascii.colorize(Colors.BOLD), "colorized(bold)")
- .isEqualTo(Colors.BOLD + ascii + Colors.BOLD)
- assertThat(null.colorize(Colors.RED), "null.colorize()").isEqualTo("")
- assertThat("".colorize(Colors.RED), "colorize()").isEqualTo("")
- assertThat(ascii.colorize(DEFAULT_COLOR), "ascii.colorize()").isEqualTo(ascii)
- assertThat(" ".colorize(Colors.NORMAL), "blank.colorize()")
- .isEqualTo(Colors.NORMAL + " " + Colors.NORMAL)
- }
-
- @Test
- fun testCyan() {
- assertThat(ascii.cyan()).isEqualTo(Colors.CYAN + ascii + Colors.NORMAL)
- }
-
- @Test
- fun testEncodeUrl() {
- assertThat("Hello Günter".encodeUrl()).isEqualTo("Hello%20G%C3%BCnter")
- }
-
- @Test
- fun testGetIntProperty() {
- val p = Properties()
- p["one"] = "1"
- p["two"] = "two"
- assertThat(p.getIntProperty("one", 9), "getIntProperty(one)").isEqualTo(1)
- assertThat(p.getIntProperty("two", 2), "getIntProperty(two)").isEqualTo(2)
- assertThat(p.getIntProperty("foo", 3), "getIntProperty(foo)").isEqualTo(3)
- }
-
- @Test
- fun testGreen() {
- assertThat(ascii.green()).isEqualTo(Colors.DARK_GREEN + ascii + Colors.NORMAL)
- }
-
- @Test
- fun testHelpCmdSyntax() {
- val bot = "mobibot"
- assertThat(helpCmdSyntax("%c $test %n $test", bot, false), "helpCmdSyntax(private)")
- .isEqualTo("$bot: $test $bot $test")
- assertThat(helpCmdSyntax("%c %n $test %c $test %n", bot, true), "helpCmdSyntax(public)")
- .isEqualTo("/msg $bot $bot $test /msg $bot $test $bot")
- }
-
- @Test
- fun testHelpFormat() {
- assertThat(helpFormat(test, isBold = true, isIndent = false), "helpFormat(bold)")
- .isEqualTo("${Colors.BOLD}$test${Colors.BOLD}")
- assertThat(helpFormat(test, isBold = false, isIndent = true), "helpFormat(indent)")
- .isEqualTo(test.prependIndent())
- assertThat(helpFormat(test, isBold = true, isIndent = true), "helpFormat(bold,indent)")
- .isEqualTo(test.colorize(Colors.BOLD).prependIndent())
-
- }
-
- @Test
- fun testIsoLocalDate() {
- assertThat(cal.time.toIsoLocalDate(), "isoLocalDate(date)").isEqualTo("1952-02-17")
- assertThat(localDateTime.toIsoLocalDate(), "isoLocalDate(localDate)").isEqualTo("1952-02-17")
- }
-
- @Test
- fun testLastOrEmpty() {
- val two = listOf("1", "2")
- assertThat(two.lastOrEmpty(), "lastOrEmpty(1,2)").isEqualTo("2")
- val one = listOf("1")
- assertThat(one.lastOrEmpty(), "lastOrEmpty(1)").isEqualTo("")
- }
-
- @Test
- fun testObfuscate() {
- assertThat(ascii.obfuscate(), "obfuscate()").all {
- length().isEqualTo(ascii.length)
- isEqualTo(("x".repeat(ascii.length)))
+ @BeforeEach
+ fun beforeEach() {
+ cal[1952, Calendar.FEBRUARY, 17, 12, 30] = 0
}
- assertThat(" ".obfuscate(), "obfuscate(blank)").isEqualTo(" ")
- }
- @Test
- fun testPlural() {
- val week = "week"
- val weeks = "weeks"
+ @Test
+ fun `Convert a Date to an ISO date`() {
+ assertThat(cal.time.toIsoLocalDate(), "isoLocalDate(date)").isEqualTo("1952-02-17")
+ }
- for (i in -1..3) {
- assertThat(week.plural(i.toLong()), "plural($i)").isEqualTo(if (i > 1) weeks else week)
+ @Test
+ fun `Convert a LocalDate to an ISO date`() {
+ assertThat(localDateTime.toIsoLocalDate(), "isoLocalDate(localDate)").isEqualTo("1952-02-17")
+ }
+
+ @Test
+ fun `Convert a Date to a UTC date-time`() {
+ assertThat(cal.time.toUtcDateTime(), "utcDateTime(date)").isEqualTo("1952-02-17 12:30")
+ }
+
+ @Test
+ fun `Convert a LocalDate to a UTC date-time`() {
+ assertThat(localDateTime.toUtcDateTime(), "utcDateTime(localDate)").isEqualTo("1952-02-17 12:30")
+ }
+
+ @Test
+ fun `Today should return the current date in ISO format`() {
+ assertThat(today()).isEqualTo(LocalDateTime.now().toIsoLocalDate())
}
}
- @Test
- fun testReplaceEach() {
- val search = arrayOf("one", "two", "three")
- val replace = arrayOf("1", "2", "3")
- assertThat(search.joinToString(",").replaceEach(search, replace), "replaceEach(1,2,3")
- .isEqualTo(replace.joinToString(","))
+ @Nested
+ @DisplayName("Help Tests")
+ inner class HelpTests {
+ private val bot = "mobibot"
- assertThat(test.replaceEach(search, replace), "replaceEach(nothing)").isEqualTo(test)
+ @Test
+ fun `Construct help string for public message`() {
+ assertThat(helpCmdSyntax("%c %n $test %c $test %n", bot, true), "helpCmdSyntax(public)")
+ .isEqualTo("/msg $bot $bot $test /msg $bot $test $bot")
+ }
- assertThat(test.replaceEach(arrayOf("t", "e"), arrayOf("", "E")), "replaceEach($test)")
- .isEqualTo(test.replace("t", "").replace("e", "E"))
+ @Test
+ fun `Construct help string for private message`() {
+ assertThat(helpCmdSyntax("%c $test %n $test", bot, false), "helpCmdSyntax(private)")
+ .isEqualTo("$bot: $test $bot $test")
+ }
- assertThat(test.replaceEach(search, emptyArray()), "replaceEach(search, empty)")
- .isEqualTo(test)
+ @Test
+ fun `Format help string with bold`() {
+ assertThat(helpFormat(test, isBold = true, isIndent = false), "helpFormat(bold)")
+ .isEqualTo("${Colors.BOLD}$test${Colors.BOLD}")
+ }
+
+ @Test
+ fun `Format help string with indent`() {
+ assertThat(helpFormat(test, isBold = false, isIndent = true), "helpFormat(indent)")
+ .isEqualTo(test.prependIndent())
+ }
+
+ @Test
+ fun `Format help string with bold and indent`() {
+ assertThat(helpFormat(test, isBold = true, isIndent = true), "helpFormat(bold,indent)")
+ .isEqualTo(test.colorize(Colors.BOLD).prependIndent())
+
+ }
}
- @Test
- fun testRed() {
- assertThat(ascii.red()).isEqualTo(ascii.colorize(Colors.RED))
+
+ @Nested
+ @DisplayName("Properties Tests")
+ inner class PropertiesTests {
+ @Test
+ fun `Convert properties to int`() {
+ assertThat(p.getIntProperty("one", 9), "getIntProperty(one)").isEqualTo(1)
+ assertThat(p.getIntProperty("two", 2), "getIntProperty(two)").isEqualTo(2)
+ }
+
+ @Test
+ fun `Convert property to int using default value`() {
+ assertThat(p.getIntProperty("foo", 3), "getIntProperty(foo)").isEqualTo(3)
+ }
}
- @Test
- fun testReverseColor() {
- assertThat(ascii.reverseColor()).isEqualTo(Colors.REVERSE + ascii + Colors.REVERSE)
+
+ @Nested
+ @DisplayName("List Tests")
+ inner class ListTests {
+ @Test
+ fun `Get last item of list`() {
+ val two = listOf("1", "2")
+ assertThat(two.lastOrEmpty(), "lastOrEmpty(1,2)").isEqualTo("2")
+ }
+
+ @Test
+ fun `Return empty if list only has one item`() {
+ val one = listOf("1")
+ assertThat(one.lastOrEmpty(), "lastOrEmpty(1)").isEqualTo("")
+ }
}
- @Test
- fun testToday() {
- assertThat(today()).isEqualTo(LocalDateTime.now().toIsoLocalDate())
- }
+ @Nested
+ @DisplayName("String Manipulation Tests")
+ inner class StringManipulationTests {
+ private val dir = "dir"
+ private val sep = '/'
+ private val url = "https://erik.thauvin.net"
- @Test
- fun testToIntOrDefault() {
- assertThat("10".toIntOrDefault(1), "toIntOrDefault(10, 1)").isEqualTo(10)
- assertThat("a".toIntOrDefault(2), "toIntOrDefault(a, 2)").isEqualTo(2)
- }
+ @Nested
+ @DisplayName("Appending Tests")
+ inner class AppendingTests {
+ @Test
+ fun `Append separator char if missing`() {
+ assertThat(dir.appendIfMissing(File.separatorChar), "appendIfMissing(dir)")
+ .isEqualTo(dir + File.separatorChar)
+ }
- @Test
- fun testUnderline() {
- assertThat(ascii.underline()).isEqualTo(ascii.colorize(Colors.UNDERLINE))
- }
+ @Test
+ fun `Append separator char if already present`() {
+ assertThat(url.appendIfMissing(sep), "appendIfMissing(url)").isEqualTo("$url$sep")
+ }
- @Test
- fun testUnescapeXml() {
- assertThat("<a name="test & ''">".unescapeXml()).isEqualTo(
- ""
- )
+ @Test
+ fun `Append separator char if not present`() {
+ assertThat("$url$sep".appendIfMissing(sep), "appendIfMissing($url$sep)").isEqualTo("$url$sep")
+ }
+ }
+
+ @Nested
+ @DisplayName("Capitalization Tests")
+ inner class CapitalizationTests {
+ @Test
+ fun `Capitalize string`() {
+ assertThat("test".capitalize(), "capitalize(test)").isEqualTo("Test")
+ }
+
+ @Test
+ fun `Capitalize string already capitalized`() {
+ assertThat("Test".capitalize(), "capitalize(Test)").isEqualTo("Test")
+ }
+
+ @Test
+ fun `Capitalize string with spaces`() {
+ assertThat(test.capitalize(), "capitalize($test)").isEqualTo(test)
+ }
+
+ @Test
+ fun `Capitalize empty string`() {
+ assertThat("".capitalize(), "capitalize()").isEqualTo("")
+ }
+
+ @Test
+ fun `Capitalize words`() {
+ assertThat(test.capitalizeWords(), "capitalizeWords(test)").isEqualTo("This Is A Test.")
+ }
+
+ @Test
+ fun `Capitalize words already capitalized`() {
+ assertThat("Already Capitalized".capitalizeWords(), "already capitalized")
+ .isEqualTo("Already Capitalized")
+ }
+
+ @Test
+ fun `Capitalize words with leading and ending spaces`() {
+ assertThat(" a test ".capitalizeWords(), "with spaces").isEqualTo(" A Test ")
+ }
+ }
+
+ @Nested
+ @DisplayName("Conversion Tests")
+ inner class ConversionTests {
+ @Test
+ fun `Convert string to int`() {
+ assertThat("10".toIntOrDefault(1), "toIntOrDefault(10, 1)").isEqualTo(10)
+ }
+
+ @Test
+ fun `Convert string to int using default value`() {
+ assertThat("a".toIntOrDefault(2), "toIntOrDefault(a, 2)").isEqualTo(2)
+ }
+ }
+
+ @Test
+ fun `Encode URL`() {
+ assertThat("Hello Günter".encodeUrl()).isEqualTo("Hello%20G%C3%BCnter")
+ }
+
+ @Nested
+ @DisplayName("Obfuscation Tests")
+ inner class ObfuscationTests {
+ @Test
+ fun `Obfuscate string`() {
+ assertThat(ascii.obfuscate(), "obfuscate()").all {
+ length().isEqualTo(ascii.length)
+ isEqualTo(("x".repeat(ascii.length)))
+ }
+ }
+
+ @Test
+ fun `Obfuscate empty string`() {
+ assertThat(" ".obfuscate(), "obfuscate(blank)").isEqualTo(" ")
+ }
+ }
+
+ @Test
+ fun `Pluralize string`() {
+ val week = "week"
+ val weeks = "weeks"
+
+ for (i in -1..3) {
+ assertThat(week.plural(i.toLong()), "plural($i)").isEqualTo(if (i > 1) weeks else week)
+ }
+ }
+
+ @Nested
+ @DisplayName("Replace Tests")
+ inner class ReplaceTests {
+
+ private val replace = arrayOf("1", "2", "3")
+
+ private val search = arrayOf("one", "two", "three")
+
+ @Test
+ fun `Replace occurrences in string`() {
+ assertThat(search.joinToString(",").replaceEach(search, replace), "replaceEach(1,2,3")
+ .isEqualTo(replace.joinToString(","))
+ }
+
+ @Test
+ fun `Replace occurrences not found in string`() {
+ assertThat(test.replaceEach(search, replace), "replaceEach(nothing)").isEqualTo(test)
+ }
+
+ @Test
+ fun `Replace and remove occurrences in string`() {
+ assertThat(test.replaceEach(arrayOf("t", "e"), arrayOf("", "E")), "replaceEach($test)")
+ .isEqualTo(test.replace("t", "").replace("e", "E"))
+ }
+
+ @Test
+ fun `Replace empty occurrences in string`() {
+ assertThat(test.replaceEach(search, emptyArray()), "replaceEach(search, empty)")
+ .isEqualTo(test)
+ }
+ }
+
+ @Test
+ fun `Unescape XML`() {
+ assertThat("<a name="test & ''">".unescapeXml()).isEqualTo(
+ ""
+ )
+ }
}
@Test
@Throws(IOException::class)
- fun testUrlReader() {
+ fun `URL reader`() {
val reader = URL("https://postman-echo.com/status/200").reader()
assertThat(reader.body).isEqualTo("{\n \"status\": 200\n}")
assertThat(reader.responseCode).isEqualTo(200)
}
- @Test
- fun testUtcDateTime() {
- assertThat(cal.time.toUtcDateTime(), "utcDateTime(date)").isEqualTo("1952-02-17 12:30")
- assertThat(localDateTime.toUtcDateTime(), "utcDateTime(localDate)").isEqualTo("1952-02-17 12:30")
+ @Nested
+ @DisplayName("Text Styling Tests")
+ inner class TextStylingTests {
+ @Nested
+ @DisplayName("Colorize Tests")
+ inner class ColorizeTests {
+ @Test
+ fun `Colorize ASCII characters red`() {
+ assertThat(ascii.colorize(Colors.RED), "red.colorize()")
+ .isEqualTo(Colors.RED + ascii + Colors.NORMAL)
+ }
+
+ @Test
+ fun `Colorize blank string`() {
+ assertThat(" ".colorize(Colors.NORMAL), "blank.colorize()")
+ .isEqualTo(Colors.NORMAL + " " + Colors.NORMAL)
+ }
+
+ @Test
+ fun `Colorize default color`() {
+ assertThat(ascii.colorize(DEFAULT_COLOR), "ascii.colorize()").isEqualTo(ascii)
+ }
+
+ @Test
+ fun `Colorize empty string`() {
+ assertThat("".colorize(Colors.RED), "colorize()").isEqualTo("")
+ }
+
+ @Test
+ fun `Colorize null`() {
+ assertThat(null.colorize(Colors.RED), "null.colorize()").isEqualTo("")
+ }
+ }
+
+ @Nested
+ @DisplayName("Color Formatting Tests")
+ inner class ColorFormattingTests {
+ @Test
+ fun `Make ASCII characters bold`() {
+ assertThat(ascii.bold(), "ascii.bold()").isEqualTo(Colors.BOLD + ascii + Colors.BOLD)
+ }
+
+ @Test
+ fun `Make int bold`() {
+ assertThat(1.bold(), "bold(1)").isEqualTo(Colors.BOLD + "1" + Colors.BOLD)
+ }
+
+ @Test
+ fun `Make long bold`() {
+ assertThat(2L.bold(), "bold(2L)").isEqualTo(Colors.BOLD + "2" + Colors.BOLD)
+ }
+
+ @Test
+ fun `Make string bold`() {
+ assertThat("test".bold(), "test.bold()").isEqualTo(Colors.BOLD + "test" + Colors.BOLD)
+ }
+
+ @Test
+ fun `Make text cyan`() {
+ assertThat(ascii.cyan()).isEqualTo(Colors.CYAN + ascii + Colors.NORMAL)
+ }
+
+ @Test
+ fun `Make text green`() {
+ assertThat(ascii.green()).isEqualTo(Colors.DARK_GREEN + ascii + Colors.NORMAL)
+ }
+
+ @Test
+ fun `Make text red`() {
+ assertThat(ascii.red()).isEqualTo(ascii.colorize(Colors.RED))
+ }
+ }
+
+ @Test
+ fun `Reversed text`() {
+ assertThat(ascii.reverseColor()).isEqualTo(Colors.REVERSE + ascii + Colors.REVERSE)
+ }
+
+ @Test
+ fun `Underline text`() {
+ assertThat(ascii.underline()).isEqualTo(ascii.colorize(Colors.UNDERLINE))
+ }
}
}
diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/commands/InfoTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/commands/InfoTest.kt
index f332005..0a28d17 100644
--- a/src/test/kotlin/net/thauvin/erik/mobibot/commands/InfoTest.kt
+++ b/src/test/kotlin/net/thauvin/erik/mobibot/commands/InfoTest.kt
@@ -34,25 +34,72 @@ package net.thauvin.erik.mobibot.commands
import assertk.assertThat
import assertk.assertions.isEqualTo
import net.thauvin.erik.mobibot.commands.Info.Companion.toUptime
+import org.junit.jupiter.api.DisplayName
+import org.junit.jupiter.api.Nested
import kotlin.test.Test
class InfoTest {
- @Test
- fun testToUptime() {
- assertThat(
- 547800300076L.toUptime(),
- "upTime(full)"
- ).isEqualTo("17 years 4 months 2 weeks 1 day 6 hours 45 minutes")
- assertThat(24300000L.toUptime(), "upTime(hours minutes)").isEqualTo("6 hours 45 minutes")
- assertThat(110700000L.toUptime(), "upTime(days hours minutes)").isEqualTo("1 day 6 hours 45 minutes")
- assertThat(
- 1320300000L.toUptime(),
- "upTime(weeks days hours minutes)"
- ).isEqualTo("2 weeks 1 day 6 hours 45 minutes")
- assertThat(2700000L.toUptime(), "upTime(45 minutes)").isEqualTo("45 minutes")
- assertThat(60000L.toUptime(), "upTime(1 minute)").isEqualTo("1 minute")
- assertThat(59000L.toUptime(), "upTime(59 seconds)").isEqualTo("59 seconds")
- assertThat(0L.toUptime(), "upTime(0 second)").isEqualTo("0 second")
+ @Nested
+ @DisplayName("Uptime Tests")
+ inner class UptimeTests {
+ @Test
+ fun `Years, Months, Weeks, Days, Hours and Minutes`() {
+ assertThat(547800300076L.toUptime()).isEqualTo("17 years 4 months 2 weeks 1 day 6 hours 45 minutes")
+ }
+ @Test
+ fun `Hours and Minutes`() {
+ assertThat(24300000L.toUptime()).isEqualTo("6 hours 45 minutes")
+ }
+
+ @Test
+ fun `Days, Hours and Minutes`() {
+ assertThat(110700000L.toUptime(), "upTime(days hours minutes)").isEqualTo("1 day 6 hours 45 minutes")
+ }
+
+ @Test
+ fun `Weeks, Days, Hours and Minutes`() {
+ assertThat(1320300000L.toUptime()).isEqualTo("2 weeks 1 day 6 hours 45 minutes")
+ }
+
+ @Test
+ fun `1 Month`() {
+ assertThat(2592000000L.toUptime(), "upTime(1 month)").isEqualTo("1 month")
+ }
+
+ @Test
+ fun `3 Days`() {
+ assertThat(259200000L.toUptime(), "upTime(3 days)").isEqualTo("3 days")
+ }
+
+ @Test
+ fun `1 Week`() {
+ assertThat(604800000L.toUptime(), "upTime(1 week)").isEqualTo("1 week")
+ }
+
+ @Test
+ fun `2 Hours`() {
+ assertThat(7200000L.toUptime(), "upTime(2 hours)").isEqualTo("2 hours")
+ }
+
+ @Test
+ fun `45 Minutes`() {
+ assertThat(2700000L.toUptime(), "upTime(45 minutes)").isEqualTo("45 minutes")
+ }
+
+ @Test
+ fun `1 Minute`() {
+ assertThat(60000L.toUptime(), "upTime(1 minute)").isEqualTo("1 minute")
+ }
+
+ @Test
+ fun `59 Seconds`() {
+ assertThat(59000L.toUptime(), "upTime(59 seconds)").isEqualTo("59 seconds")
+ }
+
+ @Test
+ fun `0 Second`() {
+ assertThat(0L.toUptime(), "upTime(0 second)").isEqualTo("0 second")
+ }
}
}
diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/commands/RecapTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/commands/RecapTest.kt
index ef6f461..7d8b86f 100644
--- a/src/test/kotlin/net/thauvin/erik/mobibot/commands/RecapTest.kt
+++ b/src/test/kotlin/net/thauvin/erik/mobibot/commands/RecapTest.kt
@@ -41,7 +41,7 @@ import kotlin.test.Test
class RecapTest {
@Test
- fun storeRecapTest() {
+ fun storeRecap() {
for (i in 1..20) {
Recap.storeRecap("sender$i", "test $i", false)
}
@@ -54,7 +54,7 @@ class RecapTest {
}
Recap.storeRecap("sender", "test action", true)
- assertThat(Recap.recaps.last())
+ assertThat(Recap.recaps.last(), "Recap.recaps.last()")
.matches("[1-2]\\d{3}-[01]\\d-[0-3]\\d [0-2]\\d:[0-6]\\d - sender test action".toRegex())
}
}
diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/commands/links/LinksManagerTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/commands/links/LinksManagerTest.kt
index 676c5b6..0a42128 100644
--- a/src/test/kotlin/net/thauvin/erik/mobibot/commands/links/LinksManagerTest.kt
+++ b/src/test/kotlin/net/thauvin/erik/mobibot/commands/links/LinksManagerTest.kt
@@ -33,45 +33,122 @@ package net.thauvin.erik.mobibot.commands.links
import assertk.all
import assertk.assertThat
-import assertk.assertions.contains
-import assertk.assertions.isEqualTo
-import assertk.assertions.isTrue
-import assertk.assertions.size
+import assertk.assertions.*
import net.thauvin.erik.mobibot.Constants
+import org.junit.jupiter.api.DisplayName
+import org.junit.jupiter.api.Nested
import kotlin.test.Test
class LinksManagerTest {
private val linksManager = LinksManager()
- @Test
- fun fetchTitle() {
- assertThat(linksManager.fetchTitle("https://erik.thauvin.net/"), "fetchTitle(Erik)").contains("Erik's Weblog")
- assertThat(
- linksManager.fetchTitle("https://www.google.com/foo"),
- "fetchTitle(Foo)"
- ).isEqualTo(Constants.NO_TITLE)
+ @Nested
+ @DisplayName("Fetch Page Title Tests")
+ inner class FetchPageTitleTests {
+ @Test
+ fun fetchPageTitle() {
+ assertThat(linksManager.fetchPageTitle("https://erik.thauvin.net/")).contains("Erik's Weblog")
+ }
+
+ @Test
+ fun fetchPageNoTitle() {
+ assertThat(linksManager.fetchPageTitle("https://www.google.com/foo")).isEqualTo(Constants.NO_TITLE)
+ }
}
- @Test
- fun testMatches() {
- assertThat(linksManager.matches("https://www.example.com/"), "matches(url)").isTrue()
- assertThat(linksManager.matches("HTTP://erik.thauvin.net/blog/ Erik's Weblog"), "matches(HTTP)").isTrue()
- }
+ @Nested
+ @DisplayName("Match Tests")
+ inner class MatchTests {
+ @Nested
+ @DisplayName("Link Tests")
+ inner class LinkTests {
+ @Test
+ @Suppress("HttpUrlsUsage")
+ fun matchInsecureLink() {
+ assertThat(linksManager.matches("http://erik.thauvin.net/blog/ Erik's Weblog")).isTrue()
+ }
- @Test
- fun matchTagKeywordsTest() {
- linksManager.setProperty(LinksManager.KEYWORDS_PROP, "key1 key2,key3")
- val tags = mutableListOf()
+ @Test
+ fun matchInvalidProtocol() {
+ assertThat(linksManager.matches("ftp://erik.thauvin.net/blog/")).isFalse()
+ }
- linksManager.matchTagKeywords("Test title with key2", tags)
- assertThat(tags, "tags").contains("key2")
- tags.clear()
+ @Test
+ fun matchLink() {
+ assertThat(linksManager.matches("https://erik.thauvin.net/blog/")).isTrue()
+ }
- linksManager.matchTagKeywords("Test key3 title with key1", tags)
- assertThat(tags, "tags(key1, key3)").all {
- contains("key1")
- contains("key3")
- size().isEqualTo(2)
+ @Test
+ fun matchLinkWithAnchor() {
+ assertThat(linksManager.matches("https://erik.thauvin.net/blog/search?tag=java#foo")).isTrue()
+ }
+
+ @Test
+ fun matchLinkWithParams() {
+ assertThat(linksManager.matches("https://erik.thauvin.net/blog/search?tag=bld&cat=java")).isTrue()
+ }
+
+ @Test
+ fun matchLinkWithSingleParam() {
+ assertThat(linksManager.matches("https://erik.thauvin.net/blog/search?tag=java")).isTrue()
+ }
+
+ @Test
+ fun matchLinkWithTitle() {
+ assertThat(linksManager.matches("https://erik.thauvin.net/blog/ Erik's Weblog")).isTrue()
+ }
+
+ @Test
+ fun matchLinkWithWhitespace() {
+ assertThat(linksManager.matches("https://erik.thauvin.net/blog/ ")).isTrue()
+ }
+
+ @Test
+ fun matchMixedCaseLink() {
+ assertThat(linksManager.matches("https://Erik.Thauvin.Net/blog/")).isTrue()
+ }
+
+ @Test
+ fun matchNonURLText() {
+ assertThat(linksManager.matches("This is just a text string")).isFalse()
+ }
+
+ @Test
+ fun matchNumericURL() {
+ assertThat(linksManager.matches("https://123.456.789.0/")).isTrue()
+ }
+
+ @Test
+ fun matchSpecialCharacterLink() {
+ assertThat(linksManager.matches("https://erik.thauvin.net/blog/search?tag=java&name=%20foo")).isTrue()
+ }
+
+ @Test
+ fun matchUpperCaseLink() {
+ assertThat(linksManager.matches("HTTPS://ERIK.THAUVIN.NET/BLOG/")).isTrue()
+ }
+ }
+
+ @Nested
+ @DisplayName("Tags Parsing Tests")
+ inner class TagsParsingTests {
+ @Test
+ fun matchTagSingleKeyword() {
+ linksManager.setProperty(LinksManager.KEYWORDS_PROP, "key1 key2,key3")
+ val tags = mutableListOf()
+ linksManager.matchTagKeywords("Test title with key2", tags)
+ assertThat(tags, "tags").containsExactly("key2")
+ }
+
+ @Test
+ fun matchTagKeywords() {
+ val tags = mutableListOf("key1", "key3")
+ linksManager.matchTagKeywords("Test key3 title with key1", tags)
+ assertThat(tags, "tags(key1, key3)").all {
+ containsExactlyInAnyOrder("key1", "key3")
+ size().isEqualTo(2)
+ }
+ }
}
}
}
diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/commands/links/ViewTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/commands/links/ViewTest.kt
index abf8224..682db91 100644
--- a/src/test/kotlin/net/thauvin/erik/mobibot/commands/links/ViewTest.kt
+++ b/src/test/kotlin/net/thauvin/erik/mobibot/commands/links/ViewTest.kt
@@ -36,76 +36,103 @@ import assertk.assertThat
import assertk.assertions.isEqualTo
import assertk.assertions.prop
import net.thauvin.erik.mobibot.entries.EntryLink
+import org.junit.jupiter.api.DisplayName
+import org.junit.jupiter.api.Nested
import kotlin.test.Test
class ViewTest {
- @Test
- fun testParseArgs() {
- val view = View()
-
- for (i in 1..10) {
- LinksManager.entries.links.add(
- EntryLink(
- "https://www.example.com/$i",
- "Example $i",
- "nick$i",
- "login$i",
- "#channel",
- emptyList()
+ companion object {
+ init {
+ for (i in 1..10) {
+ LinksManager.entries.links.add(
+ EntryLink(
+ "https://www.example.com/$i",
+ "Example $i",
+ "nick$i",
+ "login$i",
+ "#channel",
+ emptyList()
+ )
)
- )
+ }
+ }
+ }
+
+ @Nested
+ @DisplayName("Parse Args Tests")
+ inner class ParseArgsTests {
+ private val view = View()
+
+ @Test
+ fun `Parse alphanumeric query`() {
+ assertThat(view.parseArgs("1a"), "parseArgs(1a)").all {
+ prop(Pair::first).isEqualTo(0)
+ prop(Pair::second).isEqualTo("1a")
+ }
}
- assertThat(view.parseArgs("1"), "parseArgs(1)").all {
- prop(Pair::first).isEqualTo(0)
- prop(Pair::second).isEqualTo("")
+ @Test
+ fun `Parse empty arguments`() {
+ assertThat(view.parseArgs(""), "parseArgs()").all {
+ prop(Pair::first).isEqualTo(LinksManager.entries.links.size - View.MAX_ENTRIES)
+ prop(Pair::second).isEqualTo("")
+ }
}
- assertThat(view.parseArgs("2 foo"), "parseArgs(2, foo)").all {
- prop(Pair::first).isEqualTo(1)
- prop(Pair::second).isEqualTo("foo")
+ @Test
+ fun `Parse first item`() {
+ assertThat(view.parseArgs("1"), "parseArgs(1)").all {
+ prop(Pair::first).isEqualTo(0)
+ prop(Pair::second).isEqualTo("")
+ }
}
- assertThat(view.parseArgs("3 FOO"), "parseArgs(3, FOO)").all {
- prop(Pair::first).isEqualTo(2)
- prop(Pair::second).isEqualTo("foo")
+ @Test
+ fun `Parse fourth item with query needing trimming`() {
+ assertThat(view.parseArgs(" 4 foo bar "), "parseArgs( 4 foo bar )").all {
+ prop(Pair::first).isEqualTo(3)
+ prop(Pair::second).isEqualTo("foo bar")
+ }
}
- assertThat(view.parseArgs(" 4 foo bar "), "parseArgs( 4 foo bar )").all {
- prop(Pair::first).isEqualTo(3)
- prop(Pair::second).isEqualTo("foo bar")
+ @Test
+ fun `Parse overflowed item as query`() {
+ assertThat(view.parseArgs("${Int.MAX_VALUE}1"), "parseArgs(overflow)").all {
+ prop(Pair::first).isEqualTo(0)
+ prop(Pair::second).isEqualTo("${Int.MAX_VALUE}1")
+ }
}
- assertThat(view.parseArgs("foo bar"), "parseArgs(foo bar)").all {
- prop(Pair::first).isEqualTo(0)
- prop(Pair::second).isEqualTo("foo bar")
+ @Test
+ fun `Parse out of bounds item`() {
+ assertThat(view.parseArgs("20"), "parseArgs(20)").all {
+ prop(Pair::first).isEqualTo(0)
+ prop(Pair::second).isEqualTo("")
+ }
}
- assertThat(view.parseArgs("${Int.MAX_VALUE}1"), "parseArgs(overflow)").all {
- prop(Pair::first).isEqualTo(0)
- prop(Pair::second).isEqualTo("${Int.MAX_VALUE}1")
+ @Test
+ fun `Parse query only`() {
+ assertThat(view.parseArgs("foo bar"), "parseArgs(foo bar)").all {
+ prop(Pair::first).isEqualTo(0)
+ prop(Pair::second).isEqualTo("foo bar")
+ }
}
- assertThat(view.parseArgs("1a"), "parseArgs(1a)").all {
- prop(Pair::first).isEqualTo(0)
- prop(Pair::second).isEqualTo("1a")
+ @Test
+ fun `Parse second item with query`() {
+ assertThat(view.parseArgs("2 foo"), "parseArgs(2, foo)").all {
+ prop(Pair::first).isEqualTo(1)
+ prop(Pair::second).isEqualTo("foo")
+ }
}
- assertThat(view.parseArgs("20"), "parseArgs(20)").all {
- prop(Pair::first).isEqualTo(0)
- prop(Pair::second).isEqualTo("")
- }
-
- assertThat(view.parseArgs(""), "parseArgs()").all {
- prop(Pair::first).isEqualTo(LinksManager.entries.links.size - View.MAX_ENTRIES)
- prop(Pair::second).isEqualTo("")
- }
-
- LinksManager.entries.links.clear()
-
- assertThat(view.parseArgs("4"), "parseArgs(4)").all {
- prop(Pair::first).isEqualTo(0)
- prop(Pair::second).isEqualTo("")
+ @Test
+ fun `Parse third item with query ignoring capitalization`() {
+ assertThat(view.parseArgs("3 FOO"), "parseArgs(3, FOO)").all {
+ prop(Pair::first).isEqualTo(2)
+ prop(Pair::second).isEqualTo("foo")
+ }
}
}
}
diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/commands/seen/SeenTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/commands/seen/SeenTest.kt
index 7b946dc..cc01cb5 100644
--- a/src/test/kotlin/net/thauvin/erik/mobibot/commands/seen/SeenTest.kt
+++ b/src/test/kotlin/net/thauvin/erik/mobibot/commands/seen/SeenTest.kt
@@ -34,60 +34,50 @@ package net.thauvin.erik.mobibot.commands.seen
import assertk.all
import assertk.assertThat
import assertk.assertions.*
-import org.junit.AfterClass
-import org.junit.BeforeClass
+import org.junit.jupiter.api.MethodOrderer
import org.junit.jupiter.api.Order
-import kotlin.io.path.deleteIfExists
-import kotlin.io.path.fileSize
+import org.junit.jupiter.api.TestMethodOrder
import kotlin.test.Test
+@TestMethodOrder(MethodOrderer.OrderAnnotation::class)
class SeenTest {
+ companion object {
+ private val tmpFile = kotlin.io.path.createTempFile(SeenTest::class.java.simpleName, suffix = ".ser")
+ private val seen = Seen(tmpFile.toAbsolutePath().toString())
+ private const val NICK = "ErikT"
+
+ init {
+ tmpFile.toFile().deleteOnExit()
+ }
+ }
+
@Test
@Order(1)
- fun loadTest() {
+ fun add() {
+ val last = seen.seenNicks[NICK]?.lastSeen
+ seen.add(NICK)
+ assertThat(seen).all {
+ prop(Seen::seenNicks).size().isEqualTo(1)
+ prop(Seen::seenNicks).key(NICK).isNotNull().prop(SeenNick::lastSeen).isNotEqualTo(last)
+ prop(Seen::seenNicks).key(NICK).isNotNull().prop(SeenNick::nick).isNotNull().isEqualTo(NICK)
+ }
+ }
+
+ @Test
+ @Order(2)
+ fun load() {
seen.clear()
assertThat(seen::seenNicks).isEmpty()
seen.load()
assertThat(seen::seenNicks).key(NICK).isNotNull()
}
- @Test
- @Order(2)
- fun addTest() {
- val last = seen.seenNicks[NICK]?.lastSeen
- seen.add(NICK.lowercase())
- assertThat(seen).all {
- prop(Seen::seenNicks).size().isEqualTo(1)
- prop(Seen::seenNicks).key(NICK).isNotNull().prop(SeenNick::lastSeen).isNotEqualTo(last)
- prop(Seen::seenNicks).key(NICK).isNotNull().prop(SeenNick::nick).isNotNull().isEqualTo(NICK.lowercase())
- }
- }
-
@Test
@Order(3)
- fun clearTest() {
+ fun clear() {
seen.clear()
seen.save()
seen.load()
- assertThat(seen::seenNicks).size().isEqualTo(0)
- }
-
- companion object {
- private val tmpFile = kotlin.io.path.createTempFile(suffix = ".ser")
- private val seen = Seen(tmpFile.toAbsolutePath().toString())
- private const val NICK = "ErikT"
-
- @JvmStatic
- @BeforeClass
- fun beforeClass() {
- seen.add(NICK)
- assertThat(tmpFile.fileSize(), "tmpFile.size").isGreaterThan(0)
- }
-
- @JvmStatic
- @AfterClass
- fun afterClass() {
- tmpFile.deleteIfExists()
- }
+ assertThat(seen::seenNicks).isEmpty()
}
}
diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/commands/tell/TellMessageTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/commands/tell/TellMessageTest.kt
index 443c1f9..ec0c5b8 100644
--- a/src/test/kotlin/net/thauvin/erik/mobibot/commands/tell/TellMessageTest.kt
+++ b/src/test/kotlin/net/thauvin/erik/mobibot/commands/tell/TellMessageTest.kt
@@ -47,7 +47,7 @@ class TellMessageTest {
}
@Test
- fun testTellMessage() {
+ fun validateTellMessage() {
val message = "Test message."
val recipient = "recipient"
val sender = "sender"
diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/commands/tell/TellMessagesMgrTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/commands/tell/TellMessagesMgrTest.kt
index 6d3bb6b..8e8fac8 100644
--- a/src/test/kotlin/net/thauvin/erik/mobibot/commands/tell/TellMessagesMgrTest.kt
+++ b/src/test/kotlin/net/thauvin/erik/mobibot/commands/tell/TellMessagesMgrTest.kt
@@ -34,10 +34,8 @@ package net.thauvin.erik.mobibot.commands.tell
import assertk.all
import assertk.assertThat
import assertk.assertions.*
-import org.junit.AfterClass
import java.time.LocalDateTime
import kotlin.io.path.createTempFile
-import kotlin.io.path.deleteIfExists
import kotlin.io.path.fileSize
import kotlin.test.Test
@@ -49,13 +47,21 @@ class TellMessagesMgrTest {
}
}
+ companion object {
+ private val testFile = createTempFile(TellMessagesMgrTest::class.java.simpleName, suffix = ".ser")
+
+ init {
+ testFile.toFile().deleteOnExit()
+ }
+ }
+
init {
TellManager.save(testFile.toAbsolutePath().toString(), testMessages)
assertThat(testFile.fileSize()).isGreaterThan(0)
}
@Test
- fun cleanTest() {
+ fun clean() {
testMessages.add(TellMessage("sender", "recipient", "message").apply {
queued = LocalDateTime.now().minusDays(maxDays)
})
@@ -66,7 +72,7 @@ class TellMessagesMgrTest {
}
@Test
- fun loadTest() {
+ fun load() {
val messages = TellManager.load(testFile.toAbsolutePath().toString())
for (i in messages.indices) {
assertThat(messages).index(i).all {
@@ -76,14 +82,4 @@ class TellMessagesMgrTest {
}
}
}
-
- companion object {
- private val testFile = createTempFile(suffix = ".ser")
-
- @JvmStatic
- @AfterClass
- fun afterClass() {
- testFile.deleteIfExists()
- }
- }
}
diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/entries/EntriesUtilsTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/entries/EntriesUtilsTest.kt
index f67a057..df02b95 100644
--- a/src/test/kotlin/net/thauvin/erik/mobibot/entries/EntriesUtilsTest.kt
+++ b/src/test/kotlin/net/thauvin/erik/mobibot/entries/EntriesUtilsTest.kt
@@ -39,6 +39,8 @@ import net.thauvin.erik.mobibot.entries.EntriesUtils.printComment
import net.thauvin.erik.mobibot.entries.EntriesUtils.printLink
import net.thauvin.erik.mobibot.entries.EntriesUtils.printTags
import net.thauvin.erik.mobibot.entries.EntriesUtils.toLinkLabel
+import org.junit.jupiter.api.DisplayName
+import org.junit.jupiter.api.Nested
import kotlin.test.Test
class EntriesUtilsTest {
@@ -58,34 +60,38 @@ class EntriesUtilsTest {
}
}
- @Test
- fun printCommentTest() {
- assertThat(printComment(0, 0, comment)).isEqualTo("${Constants.LINK_CMD}1.1: [nick] comment")
- }
-
- @Test
- fun printLinkTest() {
- for (i in links.indices) {
- assertThat(
- printLink(i - 1, links[i]), "link $i"
- ).isEqualTo("L$i: [Skynx$i] \u0002Mobitopia$i\u0002 ( \u000303https://www.mobitopia.org/$i\u000F )")
+ @Nested
+ @DisplayName("Print Tests")
+ inner class PrintTests {
+ @Test
+ fun printComment() {
+ assertThat(printComment(0, 0, comment)).isEqualTo("${Constants.LINK_CMD}1.1: [nick] comment")
}
- assertThat(links.first().addComment(comment), "addComment()").isEqualTo(0)
- assertThat(printLink(0, links.first(), isView = true), "printLink(isView=true)").contains("[+1]")
- }
+ @Test
+ fun printLink() {
+ for (i in links.indices) {
+ assertThat(
+ printLink(i - 1, links[i]), "link $i"
+ ).isEqualTo("L$i: [Skynx$i] \u0002Mobitopia$i\u0002 ( \u000303https://www.mobitopia.org/$i\u000F )")
+ }
- @Test
- fun printTagsTest() {
- for (i in links.indices) {
- assertThat(
- printTags(i - 1, links[i]), "tag $i"
- ).isEqualTo("L${i}T: tag1, tag2, tag3, tag4, tag5")
+ assertThat(links.first().addComment(comment), "addComment()").isEqualTo(0)
+ assertThat(printLink(0, links.first(), isView = true), "printLink(isView=true)").contains("[+1]")
+ }
+
+ @Test
+ fun printTags() {
+ for (i in links.indices) {
+ assertThat(
+ printTags(i - 1, links[i]), "tag $i"
+ ).isEqualTo("L${i}T: tag1, tag2, tag3, tag4, tag5")
+ }
}
}
@Test
- fun toLinkLabelTest() {
+ fun toLinkLabel() {
assertThat(1.toLinkLabel()).isEqualTo("${Constants.LINK_CMD}2")
}
}
diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/entries/EntryLinkTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/entries/EntryLinkTest.kt
index 3479108..a38a098 100644
--- a/src/test/kotlin/net/thauvin/erik/mobibot/entries/EntryLinkTest.kt
+++ b/src/test/kotlin/net/thauvin/erik/mobibot/entries/EntryLinkTest.kt
@@ -35,6 +35,8 @@ import assertk.assertThat
import assertk.assertions.*
import com.rometools.rome.feed.synd.SyndCategory
import com.rometools.rome.feed.synd.SyndCategoryImpl
+import org.junit.jupiter.api.DisplayName
+import org.junit.jupiter.api.Nested
import java.security.SecureRandom
import java.util.*
import kotlin.test.Test
@@ -46,7 +48,7 @@ class EntryLinkTest {
)
@Test
- fun testAddDeleteComment() {
+ fun `Add then delete comment`() {
var i = 0
while (i < 5) {
entryLink.addComment("c$i", "u$i")
@@ -63,7 +65,7 @@ class EntryLinkTest {
}
val r = SecureRandom()
- while (entryLink.comments.size > 0) {
+ while (entryLink.comments.isNotEmpty()) {
entryLink.deleteComment(r.nextInt(entryLink.comments.size))
}
assertThat(entryLink.comments, "hasComments()").isEmpty()
@@ -79,7 +81,7 @@ class EntryLinkTest {
}
@Test
- fun testConstructor() {
+ fun `Validate EntryLink constructor`() {
val tags = listOf(SyndCategoryImpl().apply { name = "tag1" }, SyndCategoryImpl().apply { name = "tag2" })
val link = EntryLink("link", "title", "nick", "channel", Date(), tags)
assertThat(link, "link").all {
@@ -89,7 +91,7 @@ class EntryLinkTest {
}
@Test
- fun testMatches() {
+ fun `Validate EntryLink matches`() {
assertThat(entryLink.matches("mobitopia"), "matches(mobitopia)").isTrue()
assertThat(entryLink.matches("skynx"), "match(nick)").isTrue()
assertThat(entryLink.matches("www.mobitopia.org"), "matches(url)").isTrue()
@@ -98,29 +100,52 @@ class EntryLinkTest {
assertThat(entryLink.matches(null), "matches(null)").isFalse()
}
-
- @Test
- fun testTags() {
- val tags: List = entryLink.tags
- for ((i, tag) in tags.withIndex()) {
- assertThat(tag.name, "tag.name($i)").isEqualTo("tag${i + 1}")
+ @Nested
+ @DisplayName("Validate Tags Test")
+ inner class ValidateTagsTest {
+ @Test
+ fun `Validate tags parsing in constructor`() {
+ val tags: List = entryLink.tags
+ for ((i, tag) in tags.withIndex()) {
+ assertThat(tag.name, "tag.name($i)").isEqualTo("tag${i + 1}")
+ }
+ assertThat(entryLink::tags).size().isEqualTo(5)
+ }
+
+ @Test
+ fun `Validate attempting to remove channel tag`() {
+ val link = entryLink
+ link.setTags("+mobitopia")
+ link.setTags("-mobitopia") // can't remove the channel tag
+ assertThat(
+ link.formatTags(",")
+ ).isEqualTo("tag1,tag2,tag3,tag4,tag5,mobitopia")
+ }
+
+ @Test
+ fun `Validate formatting tags with spaces`() {
+ val link = entryLink
+ link.setTags("-tag4")
+ assertThat(
+ link.formatTags(" ", ",")
+ ).isEqualTo(",tag1 tag2 tag3 tag5")
+ }
+
+ @Test
+ fun `Validate setting blank tags`() {
+ val link = entryLink
+ val size = link.tags.size
+ link.setTags(" ")
+ assertThat(link.tags, "setTags(' ')").size().isEqualTo(size)
+ }
+
+ @Test
+ fun `Validate setting empty tags`() {
+ val link = entryLink
+ val size = link.tags.size
+ link.setTags("")
+ assertThat(link.tags, "setTags(\"\")").size().isEqualTo(size)
+
}
- assertThat(entryLink::tags).size().isEqualTo(5)
- entryLink.setTags("-tag5, tag4")
- entryLink.setTags("+mobitopia")
- entryLink.setTags("-mobitopia")
- assertThat(
- entryLink.formatTags(","),
- "formatTags(',')"
- ).isEqualTo("tag1,tag2,tag3,tag4,mobitopia")
- entryLink.setTags("-tag4 tag5")
- assertThat(
- entryLink.formatTags(" ", ","), "formatTag(' ',',')"
- ).isEqualTo(",tag1 tag2 tag3 mobitopia tag5")
- val size = entryLink.tags.size
- entryLink.setTags("")
- assertThat(entryLink.tags, "setTags('')").size().isEqualTo(size)
- entryLink.setTags(" ")
- assertThat(entryLink.tags, "setTags(' ')").size().isEqualTo(size)
}
}
diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/entries/FeedMgrTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/entries/FeedMgrTest.kt
index 5803092..a8fbf0a 100644
--- a/src/test/kotlin/net/thauvin/erik/mobibot/entries/FeedMgrTest.kt
+++ b/src/test/kotlin/net/thauvin/erik/mobibot/entries/FeedMgrTest.kt
@@ -35,6 +35,10 @@ import assertk.all
import assertk.assertThat
import assertk.assertions.*
import net.thauvin.erik.mobibot.Utils.today
+import org.junit.jupiter.api.MethodOrderer
+import org.junit.jupiter.api.Order
+import org.junit.jupiter.api.TestMethodOrder
+import java.nio.file.Path
import java.nio.file.Paths
import java.util.*
import kotlin.io.path.deleteIfExists
@@ -42,19 +46,26 @@ import kotlin.io.path.fileSize
import kotlin.io.path.name
import kotlin.test.Test
+@TestMethodOrder(MethodOrderer.OrderAnnotation::class)
class FeedMgrTest {
private val entries = Entries()
private val channel = "mobibot"
+ private var currentFile: Path
+ private var backlogFile: Path
init {
entries.logsDir = "src/test/resources/"
entries.ircServer = "irc.example.com"
entries.channel = channel
entries.backlogs = "https://www.mobitopia.org/mobibot/logs"
+
+ currentFile = Paths.get("${entries.logsDir}test.xml")
+ backlogFile = Paths.get("${entries.logsDir}${today()}.xml")
}
@Test
- fun testFeedMgr() {
+ @Order(1)
+ fun loadFeed() {
// Load the feed
assertThat(FeedsManager.loadFeed(entries), "loadFeed()").isEqualTo("2021-10-31")
@@ -87,26 +98,33 @@ class FeedMgrTest {
prop(EntryLink::nick).isEqualTo("Skynx")
prop(EntryLink::date).isEqualTo(Date(1635638460000L))
}
+ }
- val currentFile = Paths.get("${entries.logsDir}test.xml")
- val backlogFile = Paths.get("${entries.logsDir}${today()}.xml")
-
- // Save the feed
+ @Test
+ @Order(2)
+ fun saveFeed() {
FeedsManager.saveFeed(entries, currentFile.name)
assertThat(currentFile, "currentFile").exists()
assertThat(backlogFile, "backlogFile").exists()
assertThat(currentFile.fileSize(), "currentFile == backlogFile").isEqualTo(backlogFile.fileSize())
+ }
- // Load the test feed
+ @Test
+ @Order(3)
+ fun loadTestFeed() {
entries.links.clear()
FeedsManager.loadFeed(entries, currentFile.name)
entries.links.forEachIndexed { i, entryLink ->
assertThat(entryLink.title, "entryLink.title[${i + 1}]").isEqualTo("Example ${i + 1}")
}
+ }
+ @Test
+ @Order(4)
+ fun deleteFeeds() {
assertThat(currentFile.deleteIfExists(), "currentFile.deleteIfExists()").isTrue()
assertThat(backlogFile.deleteIfExists(), "backlogFile.deleteIfExists()").isTrue()
}
diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/modules/CalcTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/modules/CalcTest.kt
index 140b8a1..6489d42 100644
--- a/src/test/kotlin/net/thauvin/erik/mobibot/modules/CalcTest.kt
+++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/CalcTest.kt
@@ -37,14 +37,70 @@ import assertk.assertions.isInstanceOf
import net.objecthunter.exp4j.tokenizer.UnknownFunctionOrVariableException
import net.thauvin.erik.mobibot.Utils.bold
import net.thauvin.erik.mobibot.modules.Calc.Companion.calculate
+import org.junit.jupiter.api.DisplayName
+import org.junit.jupiter.api.Nested
+import org.mockito.Mockito
+import org.pircbotx.hooks.types.GenericMessageEvent
import kotlin.test.Test
class CalcTest {
- @Test
- fun testCalculate() {
- assertThat(calculate("1 + 1"), "calculate(1+1)").isEqualTo("1+1 = ${2.bold()}")
- assertThat(calculate("1 -3"), "calculate(1-3)").isEqualTo("1-3 = ${(-2).bold()}")
- assertThat(calculate("pi+π+e+φ"), "calculate(pi+π+e+φ)").isEqualTo("pi+π+e+φ = ${"10.62".bold()}")
- assertFailure { calculate("one + one") }.isInstanceOf(UnknownFunctionOrVariableException::class.java)
+ @Nested
+ @DisplayName("Calculate Tests")
+ inner class CalculateTests {
+ @Test
+ fun `Calculate basic addition`() {
+ assertThat(calculate("1 + 1"), "calculate(1+1)").isEqualTo("1+1 = ${2.bold()}")
+ }
+
+ @Test
+ fun `Calculate basic subtraction`() {
+ assertThat(calculate("1 -3"), "calculate(1-3)").isEqualTo("1-3 = ${(-2).bold()}")
+ }
+
+ @Test
+ fun `Calculate mathematical constants`() {
+ assertThat(calculate("pi+π+e+φ"), "calculate(pi+π+e+φ)").isEqualTo("pi+π+e+φ = ${"10.62".bold()}")
+ }
+
+ @Test
+ fun `Calculate scientific notations`() {
+ assertThat(calculate("3e2 - 3e1"), "calculate(3e2-3e1 )").isEqualTo("3e2-3e1 = ${"270".bold()}")
+ }
+
+ @Test
+ fun `Calculate trigonometric functions`() {
+ assertThat(calculate("3*sin(10)-cos(2)"), "calculate(3*sin(10)-cos(2)")
+ .isEqualTo("3*sin(10)-cos(2) = ${"-1.22".bold()}")
+ }
+
+ @Test
+ fun `Empty calculation should throw exception`() {
+ assertFailure { calculate(" ") }.isInstanceOf(IllegalArgumentException::class.java)
+ }
+
+ @Test
+ fun `Invalid calculation should throw exception`() {
+ assertFailure { calculate("a + b = c") }.isInstanceOf(UnknownFunctionOrVariableException::class.java)
+ }
+ }
+
+ @Nested
+ @DisplayName("Command Response Tests")
+ inner class CommandResponseTests {
+ @Test
+ fun `Basic calculation`() {
+ val calc = Calc()
+ val event = Mockito.mock(GenericMessageEvent::class.java)
+ calc.commandResponse("channel", "calc", "1 + 1 * 2", event)
+ Mockito.verify(event, Mockito.times(1)).respond("1+1*2 = ${"3".bold()}")
+ }
+
+ @Test
+ fun `Invalid calculation`() {
+ val calc = Calc()
+ val event = Mockito.mock(GenericMessageEvent::class.java)
+ calc.commandResponse("channel", "calc", "two + two", event)
+ Mockito.verify(event, Mockito.times(1)).respond("No idea. This is the kind of math I don't get.")
+ }
}
}
diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/modules/ChatGpt2Test.kt b/src/test/kotlin/net/thauvin/erik/mobibot/modules/ChatGpt2Test.kt
index ee3e534..573dfa2 100644
--- a/src/test/kotlin/net/thauvin/erik/mobibot/modules/ChatGpt2Test.kt
+++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/ChatGpt2Test.kt
@@ -34,28 +34,62 @@ import assertk.assertFailure
import assertk.assertThat
import assertk.assertions.contains
import assertk.assertions.hasNoCause
+import assertk.assertions.isEqualTo
import assertk.assertions.isInstanceOf
import net.thauvin.erik.mobibot.DisableOnCi
import net.thauvin.erik.mobibot.LocalProperties
+import org.junit.jupiter.api.DisplayName
+import org.junit.jupiter.api.Nested
+import org.mockito.ArgumentCaptor
+import org.mockito.Mockito
+import org.pircbotx.hooks.types.GenericMessageEvent
import kotlin.test.Test
class ChatGpt2Test : LocalProperties() {
- @Test
- fun testApiKey() {
- assertFailure { ChatGpt2.chat("1 gallon to liter", "", 0) }
- .isInstanceOf(ModuleException::class.java)
- .hasNoCause()
+ @Nested
+ @DisplayName("Command Response Tests")
+ inner class CommandResponseTests {
+ @Test
+ fun moduleMisconfigured() {
+ val chatGpt2 = ChatGpt2()
+ val event = Mockito.mock(GenericMessageEvent::class.java)
+ val captor = ArgumentCaptor.forClass(String::class.java)
+
+ chatGpt2.commandResponse("channel", "chatgpt", "1 liter to gallon", event)
+
+ Mockito.verify(event, Mockito.atLeastOnce()).respond(captor.capture())
+ assertThat(captor.value).isEqualTo("The ${ChatGpt2.CHATGPT_NAME} module is misconfigured.")
+ }
}
- @Test
- @DisableOnCi
- fun testChat() {
- val apiKey = getProperty(ChatGpt2.API_KEY_PROP)
- assertThat(
- ChatGpt2.chat("how do I make an HTTP request in Javascript?", apiKey, 200)
- ).contains("XMLHttpRequest")
+ @Nested
+ @DisplayName("Chat Tests")
+ inner class ChatTests {
+ @Test
+ fun apiKey() {
+ assertFailure { ChatGpt2.chat("1 gallon to liter", "", 0) }
+ .isInstanceOf(ModuleException::class.java)
+ .hasNoCause()
+ }
+ private val apiKey = getProperty(ChatGpt2.API_KEY_PROP)
- assertFailure { ChatGpt2.chat("1 liter to gallon", apiKey, -1) }
- .isInstanceOf(ModuleException::class.java)
+ @Test
+ @DisableOnCi
+ fun chat() {
+ assertThat(
+ ChatGpt2.chat(
+ "javascript function to make a request with XMLHttpRequest, just code",
+ apiKey,
+ 50
+ )
+ ).contains("```javascript")
+ }
+
+ @Test
+ @DisableOnCi
+ fun chatFailure() {
+ assertFailure { ChatGpt2.chat("1 liter to gallon", apiKey, -1) }
+ .isInstanceOf(ModuleException::class.java)
+ }
}
}
diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/modules/CryptoPricesTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/modules/CryptoPricesTest.kt
index 94a40d9..194f798 100644
--- a/src/test/kotlin/net/thauvin/erik/mobibot/modules/CryptoPricesTest.kt
+++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/CryptoPricesTest.kt
@@ -32,47 +32,22 @@ package net.thauvin.erik.mobibot.modules
import assertk.all
import assertk.assertThat
-import assertk.assertions.isEqualTo
-import assertk.assertions.isGreaterThan
-import assertk.assertions.prop
+import assertk.assertions.*
import net.thauvin.erik.crypto.CryptoPrice
import net.thauvin.erik.mobibot.modules.CryptoPrices.Companion.currentPrice
import net.thauvin.erik.mobibot.modules.CryptoPrices.Companion.getCurrencyName
import net.thauvin.erik.mobibot.modules.CryptoPrices.Companion.loadCurrencies
import org.junit.jupiter.api.BeforeAll
+import org.junit.jupiter.api.DisplayName
+import org.junit.jupiter.api.Nested
+import org.mockito.ArgumentCaptor
+import org.mockito.Mockito
+import org.pircbotx.hooks.types.GenericMessageEvent
import java.util.logging.ConsoleHandler
import java.util.logging.Level
import kotlin.test.Test
class CryptoPricesTest {
- init {
- loadCurrencies()
- }
-
- @Test
- @Throws(ModuleException::class)
- fun testMarketPrice() {
- var price = currentPrice(listOf("BTC"))
- assertThat(price, "currentPrice(BTC)").all {
- prop(CryptoPrice::base).isEqualTo("BTC")
- prop(CryptoPrice::currency).isEqualTo("USD")
- prop(CryptoPrice::amount).transform { it.signum() }.isGreaterThan(0)
- }
-
- price = currentPrice(listOf("ETH", "EUR"))
- assertThat(price, "currentPrice(ETH, EUR)").all {
- prop(CryptoPrice::base).isEqualTo("ETH")
- prop(CryptoPrice::currency).isEqualTo("EUR")
- prop(CryptoPrice::amount).transform { it.signum() }.isGreaterThan(0)
- }
- }
-
- @Test
- fun testGetCurrencyName() {
- assertThat(getCurrencyName("USD"), "USD").isEqualTo("United States Dollar")
- assertThat(getCurrencyName("EUR"), "EUR").isEqualTo("Euro")
- }
-
companion object {
@JvmStatic
@BeforeAll
@@ -84,4 +59,89 @@ class CryptoPricesTest {
}
}
}
+
+ init {
+ loadCurrencies()
+ }
+
+ @Nested
+ @DisplayName("Command Response Tests")
+ inner class CommandResponseTests {
+ @Test
+ fun `Current price for BTC`() {
+ val cryptoPrices = CryptoPrices()
+ val event = Mockito.mock(GenericMessageEvent::class.java)
+ val captor = ArgumentCaptor.forClass(String::class.java)
+
+ cryptoPrices.commandResponse("channel", "crypto", "btc", event)
+
+ Mockito.verify(event, Mockito.times(1)).respond(captor.capture())
+ assertThat(captor.value).startsWith("BTC current price is $")
+ }
+
+ @Test
+ fun `Current price for BTC in EUR`() {
+ val cryptoPrices = CryptoPrices()
+ val event = Mockito.mock(GenericMessageEvent::class.java)
+ val captor = ArgumentCaptor.forClass(String::class.java)
+
+ cryptoPrices.commandResponse("channel", "crypto", "eth eur", event)
+
+ Mockito.verify(event, Mockito.times(1)).respond(captor.capture())
+ assertThat(captor.value).matches(Regex("ETH current price is €.* \\[Euro]"))
+ }
+
+ @Test
+ fun `Invalid crypto symbol`() {
+ val cryptoPrices = CryptoPrices()
+ val event = Mockito.mock(GenericMessageEvent::class.java)
+ val captor = ArgumentCaptor.forClass(String::class.java)
+
+ cryptoPrices.commandResponse("channel", "crypto", "foo", event)
+
+ Mockito.verify(event, Mockito.times(1)).respond(captor.capture())
+ assertThat(captor.value)
+ .isEqualTo("${CryptoPrices.DEFAULT_ERROR_MESSAGE}: not found")
+ }
+ }
+
+ @Nested
+ @DisplayName("Currency Name Tests")
+ inner class CurrencyNameTests {
+ @Test
+ fun `Currency name for USD`() {
+ assertThat(getCurrencyName("USD"), "USD").isEqualTo("United States Dollar")
+ }
+
+ @Test
+ fun `Currency name for EUR`() {
+ assertThat(getCurrencyName("EUR"), "EUR").isEqualTo("Euro")
+ }
+ }
+
+ @Nested
+ @DisplayName("Current Price Tests")
+ inner class CurrentPriceTests {
+ @Test
+ @Throws(ModuleException::class)
+ fun `Current price for Bitcoin`() {
+ val price = currentPrice(listOf("BTC"))
+ assertThat(price, "currentPrice(BTC)").all {
+ prop(CryptoPrice::base).isEqualTo("BTC")
+ prop(CryptoPrice::currency).isEqualTo("USD")
+ prop(CryptoPrice::amount).transform { it.signum() }.isGreaterThan(0)
+ }
+ }
+
+ @Test
+ @Throws(ModuleException::class)
+ fun `Current price for Ethereum in Euro`() {
+ val price = currentPrice(listOf("ETH", "EUR"))
+ assertThat(price, "currentPrice(ETH, EUR)").all {
+ prop(CryptoPrice::base).isEqualTo("ETH")
+ prop(CryptoPrice::currency).isEqualTo("EUR")
+ prop(CryptoPrice::amount).transform { it.signum() }.isGreaterThan(0)
+ }
+ }
+ }
}
diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/modules/CurrencyConverterTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/modules/CurrencyConverterTest.kt
index c1c0efc..7ee2b4d 100644
--- a/src/test/kotlin/net/thauvin/erik/mobibot/modules/CurrencyConverterTest.kt
+++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/CurrencyConverterTest.kt
@@ -32,16 +32,18 @@ package net.thauvin.erik.mobibot.modules
import assertk.all
import assertk.assertThat
-import assertk.assertions.contains
-import assertk.assertions.isInstanceOf
-import assertk.assertions.matches
-import assertk.assertions.prop
+import assertk.assertions.*
import net.thauvin.erik.mobibot.LocalProperties
import net.thauvin.erik.mobibot.modules.CurrencyConverter.Companion.convertCurrency
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
+import org.junit.jupiter.api.DisplayName
+import org.junit.jupiter.api.Nested
+import org.mockito.ArgumentCaptor
+import org.mockito.Mockito
+import org.pircbotx.hooks.types.GenericMessageEvent
import kotlin.test.Test
class CurrencyConverterTest : LocalProperties() {
@@ -50,28 +52,80 @@ class CurrencyConverterTest : LocalProperties() {
loadSymbols(apiKey)
}
- @Test
- fun testConvertCurrency() {
- val apiKey = getProperty(CurrencyConverter.API_KEY_PROP)
- assertThat(
- convertCurrency(apiKey, "100 USD to EUR").msg,
- "convertCurrency(100 USD to EUR)"
- ).matches("100 United States Dollar = \\d{2,3}\\.\\d{2,3} Euro".toRegex())
- assertThat(
- convertCurrency(apiKey, "1 USD to GBP").msg,
- "convertCurrency(1 USD to BGP)"
- ).matches("1 United States Dollar = 0\\.\\d{2,3} Pound Sterling".toRegex())
- assertThat(
- convertCurrency(apiKey, "100,000.00 CAD to USD").msg,
- "convertCurrency(100,000.00 GBP to USD)"
- ).matches("100,000.00 Canadian Dollar = \\d+\\.\\d{2,3} United States Dollar".toRegex())
- assertThat(convertCurrency(apiKey, "100 USD to USD"), "convertCurrency(100 USD to USD)").all {
- prop(Message::msg).contains("You're kidding, right?")
- isInstanceOf(PublicMessage::class.java)
+ @Nested
+ @DisplayName("Command Response Tests")
+ inner class CommandResponseTests {
+ @Test
+ fun `USD to CAD`() {
+ val currencyConverter = CurrencyConverter()
+ val event = Mockito.mock(GenericMessageEvent::class.java)
+ val captor = ArgumentCaptor.forClass(String::class.java)
+
+ currencyConverter.properties.put(
+ CurrencyConverter.API_KEY_PROP, getProperty(CurrencyConverter.API_KEY_PROP)
+ )
+ currencyConverter.commandResponse("channel", "currency", "1 USD to CAD", event)
+
+ Mockito.verify(event, Mockito.atLeastOnce()).respond(captor.capture())
+ assertThat(captor.value).matches("1 United States Dollar = \\d+\\.\\d{2,3} Canadian Dollar".toRegex())
}
- assertThat(convertCurrency(apiKey, "100 USD"), "convertCurrency(100 USD)").all {
- prop(Message::msg).contains("Invalid query.")
- isInstanceOf(ErrorMessage::class.java)
+
+ @Test
+ fun `API Key is not specified`() {
+ val currencyConverter = CurrencyConverter()
+ val event = Mockito.mock(GenericMessageEvent::class.java)
+ val captor = ArgumentCaptor.forClass(String::class.java)
+
+ currencyConverter.commandResponse("channel", "currency", "1 USD to CAD", event)
+
+ Mockito.verify(event, Mockito.atLeastOnce()).respond(captor.capture())
+ assertThat(captor.value).isEqualTo(CurrencyConverter.ERROR_MESSAGE_NO_API_KEY)
+ }
+ }
+
+ @Nested
+ @DisplayName("Currency Converter Tests")
+ inner class CurrencyConverterTests {
+ private val apiKey = getProperty(CurrencyConverter.API_KEY_PROP)
+
+ @Test
+ fun `Convert CAD to USD`() {
+ assertThat(
+ convertCurrency(apiKey, "100,000.00 CAD to USD").msg,
+ "convertCurrency(100,000.00 GBP to USD)"
+ ).matches("100,000.00 Canadian Dollar = \\d+\\.\\d{2,3} United States Dollar".toRegex())
+ }
+
+ @Test
+ fun `Convert USD to EUR`() {
+ assertThat(
+ convertCurrency(apiKey, "100 USD to EUR").msg,
+ "convertCurrency(100 USD to EUR)"
+ ).matches("100 United States Dollar = \\d{2,3}\\.\\d{2,3} Euro".toRegex())
+ }
+
+ @Test
+ fun `Convert USD to GBP`() {
+ assertThat(
+ convertCurrency(apiKey, "1 USD to GBP").msg,
+ "convertCurrency(1 USD to BGP)"
+ ).matches("1 United States Dollar = 0\\.\\d{2,3} Pound Sterling".toRegex())
+ }
+
+ @Test
+ fun `Convert USD to USD`() {
+ assertThat(convertCurrency(apiKey, "100 USD to USD"), "convertCurrency(100 USD to USD)").all {
+ prop(Message::msg).contains("You're kidding, right?")
+ isInstanceOf(PublicMessage::class.java)
+ }
+ }
+
+ @Test
+ fun `Invalid Query should throw exception`() {
+ assertThat(convertCurrency(apiKey, "100 USD"), "convertCurrency(100 USD)").all {
+ prop(Message::msg).contains("Invalid query.")
+ isInstanceOf(ErrorMessage::class.java)
+ }
}
}
}
diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/modules/DiceTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/modules/DiceTest.kt
index e34de7b..f8f71bc 100644
--- a/src/test/kotlin/net/thauvin/erik/mobibot/modules/DiceTest.kt
+++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/DiceTest.kt
@@ -35,19 +35,109 @@ package net.thauvin.erik.mobibot.modules
import assertk.assertThat
import assertk.assertions.isEqualTo
import assertk.assertions.matches
+import assertk.assertions.startsWith
+import org.junit.jupiter.api.DisplayName
+import org.junit.jupiter.api.Nested
+import org.junit.jupiter.api.RepeatedTest
+import org.mockito.ArgumentCaptor
+import org.mockito.Mockito
+import org.pircbotx.hooks.types.GenericMessageEvent
+import kotlin.random.Random
import kotlin.test.Test
class DiceTest {
- @Test
- fun testRoll() {
- assertThat(Dice.roll(1, 1), "roll(1d1)").isEqualTo("\u00021\u0002")
- assertThat(Dice.roll(2, 1), "roll(2d1)")
- .isEqualTo("\u00021\u0002 + \u00021\u0002 = \u00022\u0002")
- assertThat(Dice.roll(5, 1), "roll(5d1)")
- .isEqualTo("\u00021\u0002 + \u00021\u0002 + \u00021\u0002 + \u00021\u0002 + \u00021\u0002 = \u00025\u0002")
- assertThat(Dice.roll(2, 6), "roll(2d6)")
- .matches("\u0002[1-6]\u0002 \\+ \u0002[1-6]\u0002 = \u0002[1-9][0-2]?\u0002".toRegex())
- assertThat(Dice.roll(3, 7), "roll(3d7)")
- .matches("\u0002[1-7]\u0002 \\+ \u0002[1-7]\u0002 \\+ \u0002[1-7]\u0002 = \u0002\\d{1,2}\u0002".toRegex())
+ @Nested
+ @DisplayName("Command Response Tests")
+ inner class CommandResponseTests {
+ @Test
+ fun `Roll die`() {
+ val dice = Dice()
+ val event = Mockito.mock(GenericMessageEvent::class.java)
+ val captor = ArgumentCaptor.forClass(String::class.java)
+
+ dice.commandResponse("channel", "dice", "", event)
+
+ Mockito.verify(event, Mockito.times(1)).respond(captor.capture())
+ assertThat(captor.value).startsWith("you rolled")
+ }
+
+ @RepeatedTest(3)
+ fun `Roll die with 9 sides`() {
+ val dice = Dice()
+ val event = Mockito.mock(GenericMessageEvent::class.java)
+ val captor = ArgumentCaptor.forClass(String::class.java)
+
+ dice.commandResponse("channel", "dice", "1d9", event)
+
+ Mockito.verify(event, Mockito.times(1)).respond(captor.capture())
+ assertThat(captor.value).matches("you rolled \u0002[1-9]\u0002".toRegex())
+ }
+
+ @RepeatedTest(3)
+ fun `Roll dice`() {
+ val dice = Dice()
+ val event = Mockito.mock(GenericMessageEvent::class.java)
+ val captor = ArgumentCaptor.forClass(String::class.java)
+
+ dice.commandResponse("channel", "dice", "2d6", event)
+
+ Mockito.verify(event, Mockito.times(1)).respond(captor.capture())
+ assertThat(captor.value)
+ .matches("you rolled \u0002[1-6]\u0002 \\+ \u0002[1-6]\u0002 = \u0002\\d{1,2}\\u0002".toRegex())
+ }
+ }
+
+ @Nested
+ @DisplayName("Roll Tests")
+ inner class RollTests {
+ @Test
+ fun `Roll die`() {
+ assertThat(Dice.roll(1, 6)).matches("\u0002[1-6]\u0002".toRegex())
+ }
+
+ @Test
+ fun `Roll die with 1 side`() {
+ assertThat(Dice.roll(1, 1)).isEqualTo("\u00021\u0002")
+ }
+
+ @RepeatedTest(5)
+ fun `Roll die with random sides`() {
+ assertThat(Dice.roll(1, Random.nextInt(1, 11))).matches("\u0002([1-9]|10)\u0002".toRegex())
+ }
+
+ @Test
+ fun `Roll 2 dice`() {
+ assertThat(Dice.roll(2, 6))
+ .matches("\u0002[1-6]\u0002 \\+ \u0002[1-6]\u0002 = \u0002[1-9][0-2]?\u0002".toRegex())
+ }
+
+ @Test
+ fun `Roll 2 dice with 1 side`() {
+ assertThat(Dice.roll(2, 1)).isEqualTo("\u00021\u0002 + \u00021\u0002 = \u00022\u0002")
+ }
+
+ @Test
+ fun `Roll 3 dice with 1 side`() {
+ assertThat(Dice.roll(4, 1))
+ .isEqualTo("\u00021\u0002 + \u00021\u0002 + \u00021\u0002 + \u00021\u0002 = \u00024\u0002")
+ }
+
+ @Test
+ fun `Roll 3 dice with 7 sides`() {
+ assertThat(Dice.roll(3, 7))
+ .matches(
+ "\u0002[1-7]\u0002 \\+ \u0002[1-7]\u0002 \\+ \u0002[1-7]\u0002 = \u0002\\d{1,2}\u0002"
+ .toRegex()
+ )
+ }
+
+ @RepeatedTest(3)
+ fun `Roll 3 dice with random sides`() {
+ assertThat(Dice.roll(3, Random.nextInt(1, 6)))
+ .matches(
+ "\u0002[1-5]\u0002 \\+ \u0002[1-5]\u0002 \\+ \u0002[1-5]\u0002 = \u0002\\d{1,2}\u0002"
+ .toRegex()
+ )
+ }
}
}
diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/modules/Gemini2Test.kt b/src/test/kotlin/net/thauvin/erik/mobibot/modules/Gemini2Test.kt
index 269874a..b0b5a28 100644
--- a/src/test/kotlin/net/thauvin/erik/mobibot/modules/Gemini2Test.kt
+++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/Gemini2Test.kt
@@ -35,31 +35,72 @@ import assertk.assertThat
import assertk.assertions.*
import net.thauvin.erik.mobibot.DisableOnCi
import net.thauvin.erik.mobibot.LocalProperties
+import org.junit.jupiter.api.DisplayName
+import org.junit.jupiter.api.Nested
+import org.mockito.ArgumentCaptor
+import org.mockito.Mockito
+import org.pircbotx.hooks.types.GenericMessageEvent
import kotlin.test.Test
class Gemini2Test : LocalProperties() {
- @Test
- fun testApiKey() {
- assertFailure { Gemini2.chat("1 gallon to liter", "", 0) }
- .isInstanceOf(ModuleException::class.java)
- .hasNoCause()
+ @Nested
+ @DisplayName("Chat Tests")
+ inner class ChatTests {
+ private val apiKey = getProperty(Gemini2.GEMINI_API_KEY)
+ private val maxTokens = getProperty(Gemini2.MAX_TOKENS_PROP).toInt()
+
+ @Test
+ @DisableOnCi
+ fun chatHttpRequestInJavascript() {
+ assertThat(
+ Gemini2.chat(
+ "javascript function to make a request with XMLHttpRequest, just code",
+ apiKey,
+ maxTokens
+ )
+ ).isNotNull().contains("```javascript")
+ }
+
+ @Test
+ @DisableOnCi
+ fun chatEncodeUrlInJava() {
+ assertThat(
+ Gemini2.chat("encode a url in java, one line, just code", apiKey, 60)
+ ).isNotNull().contains("UrlEncoder", true)
+ }
}
- @Test
- @DisableOnCi
- fun chatPrompt() {
- val apiKey = getProperty(Gemini2.GEMINI_API_KEY)
- val maxTokens = getProperty(Gemini2.MAX_TOKENS_PROP).toInt()
+ @Nested
+ @DisplayName("Command Response Tests")
+ inner class CommandResponseTests {
+ @Test
+ fun moduleMisconfigured() {
+ val gemini2 = Gemini2()
+ val event = Mockito.mock(GenericMessageEvent::class.java)
+ val captor = ArgumentCaptor.forClass(String::class.java)
- assertThat(
- Gemini2.chat("how do I make an HTTP request in Javascript?", apiKey, maxTokens)
- ).isNotNull().contains("XMLHttpRequest")
+ gemini2.commandResponse("channel", "gemini", "1 liter to gallon", event)
- assertThat(
- Gemini2.chat("how do I encode a URL in java?", apiKey, 60)
- ).isNotNull().contains("URLEncoder")
+ Mockito.verify(event, Mockito.atLeastOnce()).respond(captor.capture())
+ assertThat(captor.value).isEqualTo("The ${Gemini2.GEMINI_NAME} module is misconfigured.")
+ }
+ }
- assertFailure { Gemini2.chat("1 liter to gallon", "foo", 40) }
- .isInstanceOf(ModuleException::class.java)
+ @Nested
+ @DisplayName("API Keys Test")
+ inner class ApiKeysTest {
+ @Test
+ fun invalidApiKey() {
+ assertFailure { Gemini2.chat("1 liter to gallon", "foo", 40) }
+ .isInstanceOf(ModuleException::class.java)
+ .hasMessage(Gemini2.IO_ERROR)
+ }
+
+ @Test
+ fun emptyApiKey() {
+ assertFailure { Gemini2.chat("1 liter to gallon", "", 40) }
+ .isInstanceOf(ModuleException::class.java)
+ .hasMessage(Gemini2.API_KEY_ERROR)
+ }
}
}
diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/modules/GoogleSearchTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/modules/GoogleSearchTest.kt
index f9b0832..0edbaa5 100644
--- a/src/test/kotlin/net/thauvin/erik/mobibot/modules/GoogleSearchTest.kt
+++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/GoogleSearchTest.kt
@@ -40,48 +40,22 @@ import net.thauvin.erik.mobibot.LocalProperties
import net.thauvin.erik.mobibot.modules.GoogleSearch.Companion.searchGoogle
import net.thauvin.erik.mobibot.msg.ErrorMessage
import net.thauvin.erik.mobibot.msg.Message
+import org.junit.jupiter.api.DisplayName
+import org.junit.jupiter.api.Nested
+import org.mockito.ArgumentCaptor
+import org.mockito.Mockito
+import org.mockito.kotlin.whenever
+import org.pircbotx.hooks.types.GenericMessageEvent
import kotlin.test.Test
class GoogleSearchTest : LocalProperties() {
- @Test
- fun testAPIKeys() {
- assertThat(
- searchGoogle("", "apikey", "cssKey").first(),
- "searchGoogle(empty)"
- ).isInstanceOf(ErrorMessage::class.java)
+ private val apiKey = getProperty(GoogleSearch.API_KEY_PROP)
+ private val cseKey = getProperty(GoogleSearch.CSE_KEY_PROP)
- assertFailure { searchGoogle("test", "", "apiKey") }
- .isInstanceOf(ModuleException::class.java).hasNoCause()
-
- assertFailure { searchGoogle("test", "apiKey", "") }
- .isInstanceOf(ModuleException::class.java).hasNoCause()
-
- assertFailure { searchGoogle("test", "apiKey", "cssKey") }
- .isInstanceOf(ModuleException::class.java)
- .hasMessage("API key not valid. Please pass a valid API key.")
- }
-
- @Test
- @DisableOnCi
@Throws(ModuleException::class)
- fun testSearchGoogle() {
- val apiKey = getProperty(GoogleSearch.API_KEY_PROP)
- val cseKey = getProperty(GoogleSearch.CSE_KEY_PROP)
-
+ fun sanitizedSearch(query: String, apiKey: String, cseKey: String): List {
try {
- var query = "mobibot"
- var messages = searchGoogle(query, apiKey, cseKey)
- assertThat(messages, "searchGoogle($query)").all {
- isNotEmpty()
- index(0).prop(Message::msg).contains(query, true)
- }
-
- query = "adadflkjl"
- messages = searchGoogle(query, apiKey, cseKey)
- assertThat(messages, "searchGoogle($query)").index(0).all {
- isInstanceOf(ErrorMessage::class.java)
- prop(Message::msg).isEqualTo("No results found.")
- }
+ return searchGoogle(query, apiKey, cseKey)
} catch (e: ModuleException) {
// Avoid displaying api keys in CI logs
if ("true" == System.getenv("CI")) {
@@ -91,4 +65,101 @@ class GoogleSearchTest : LocalProperties() {
}
}
}
+
+ @Nested
+ @DisplayName("Command Response Tests")
+ inner class CommandResponseTests {
+ @Test
+ fun `API Keys are missing`() {
+ val googleSearch = GoogleSearch()
+ val event = Mockito.mock(GenericMessageEvent::class.java)
+ val captor = ArgumentCaptor.forClass(String::class.java)
+ val user = Mockito.mock(org.pircbotx.User::class.java)
+
+ whenever(event.user).thenReturn(user)
+ whenever(user.nick).thenReturn("mock")
+
+ googleSearch.commandResponse("channel", "google", "seattle seahawks", event)
+
+ Mockito.verify(event, Mockito.atLeastOnce()).respond(captor.capture())
+ assertThat(captor.value)
+ .isEqualTo("${GoogleSearch.SERVICE_NAME} is disabled. The API keys are missing.")
+ }
+
+ @Test
+ fun `No results found`() {
+ val googleSearch = GoogleSearch()
+ val event = Mockito.mock(GenericMessageEvent::class.java)
+ val captor = ArgumentCaptor.forClass(String::class.java)
+ val user = Mockito.mock(org.pircbotx.User::class.java)
+
+ whenever(event.user).thenReturn(user)
+ whenever(user.nick).thenReturn("mock")
+
+ googleSearch.properties.put(GoogleSearch.API_KEY_PROP, apiKey)
+ googleSearch.properties.put(GoogleSearch.CSE_KEY_PROP, cseKey)
+
+ googleSearch.commandResponse("channel", "google", "\"foobarbarfoofoobarblahblah\"", event)
+
+ Mockito.verify(event, Mockito.atLeastOnce()).respond(captor.capture())
+ assertThat(captor.value)
+ .isEqualTo("\u000304No results found.\u000F")
+ }
+ }
+
+ @Nested
+ @DisplayName("API Keys Test")
+ inner class ApiKeysTest {
+ @Test
+ fun `API key should not be empty`() {
+ assertFailure { sanitizedSearch("test", "", "apiKey") }
+ .isInstanceOf(ModuleException::class.java).hasNoCause()
+ }
+
+ @Test
+ fun `CSE key should not empty`() {
+ assertFailure { sanitizedSearch("test", "apiKey", "") }
+ .isInstanceOf(ModuleException::class.java).hasNoCause()
+ }
+
+ @Test
+ fun `Invalid API key should throw exception`() {
+ assertFailure { sanitizedSearch("test", "apiKey", "cssKey") }
+ .isInstanceOf(ModuleException::class.java)
+ .hasMessage("API key not valid. Please pass a valid API key.")
+ }
+ }
+
+ @Nested
+ @DisplayName("Search Tests")
+ inner class SearchTests {
+ @Test
+ fun `Query should not be empty`() {
+ assertThat(sanitizedSearch("", apiKey, cseKey).first()).isInstanceOf(ErrorMessage::class.java)
+ }
+
+ @Test
+ @DisableOnCi
+ @Throws(ModuleException::class)
+ fun `No results found`() {
+ val query = "adadflkjl"
+ val messages = sanitizedSearch(query, apiKey, cseKey)
+ assertThat(messages, "searchGoogle($query)").index(0).all {
+ isInstanceOf(ErrorMessage::class.java)
+ prop(Message::msg).isEqualTo("No results found.")
+ }
+ }
+
+ @Test
+ @DisableOnCi
+ @Throws(ModuleException::class)
+ fun `Search Google`() {
+ val query = "mobibot"
+ val messages = sanitizedSearch(query, apiKey, cseKey)
+ assertThat(messages, "searchGoogle($query)").all {
+ isNotEmpty()
+ index(0).prop(Message::msg).contains(query, true)
+ }
+ }
+ }
}
diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/modules/JokeTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/modules/JokeTest.kt
index cf6d03c..d8c9919 100644
--- a/src/test/kotlin/net/thauvin/erik/mobibot/modules/JokeTest.kt
+++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/JokeTest.kt
@@ -41,7 +41,7 @@ import kotlin.test.Test
class JokeTest {
@Test
@Throws(ModuleException::class)
- fun testRandomJoke() {
+ fun `Get a random joke`() {
val joke = randomJoke()
assertThat(joke, "randomJoke()").all {
size().isGreaterThan(0)
diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/modules/LookupTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/modules/LookupTest.kt
index abb9235..38cbabd 100644
--- a/src/test/kotlin/net/thauvin/erik/mobibot/modules/LookupTest.kt
+++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/LookupTest.kt
@@ -33,24 +33,80 @@ package net.thauvin.erik.mobibot.modules
import assertk.assertThat
import assertk.assertions.any
import assertk.assertions.contains
+import assertk.assertions.isEqualTo
import net.thauvin.erik.mobibot.modules.Lookup.Companion.nslookup
import net.thauvin.erik.mobibot.modules.Lookup.Companion.whois
+import org.junit.jupiter.api.DisplayName
+import org.junit.jupiter.api.Nested
+import org.mockito.ArgumentCaptor
+import org.mockito.Mockito
+import org.pircbotx.hooks.types.GenericMessageEvent
import kotlin.test.Test
class LookupTest {
- @Test
- @Throws(Exception::class)
- fun testLookup() {
- var result = nslookup("apple.com")
- assertThat(result, "lookup(apple.com)").contains("17.253.144.10")
+ @Nested
+ @DisplayName("Command Response Tests")
+ inner class CommandResponseTests {
+ @Test
+ fun lookupByHostname() {
+ val lookup = Lookup()
+ val event = Mockito.mock(GenericMessageEvent::class.java)
+ val captor = ArgumentCaptor.forClass(String::class.java)
- result = nslookup("37.27.52.13")
- assertThat(result, "lookup(37.27.52.13)").contains("nix4.thauvin.us")
+ lookup.commandResponse("channel", "lookup", "ec2-54-234-237-183.compute-1.amazonaws.com", event)
+
+ Mockito.verify(event, Mockito.times(1)).respondWith(captor.capture())
+ assertThat(captor.value).contains("54.234.237.183")
+ }
+
+ @Test
+ fun lookupByAddress() {
+ val lookup = Lookup()
+ val event = Mockito.mock(GenericMessageEvent::class.java)
+ val captor = ArgumentCaptor.forClass(String::class.java)
+
+ lookup.commandResponse("channel", "lookup", "54.234.237.183", event)
+
+ Mockito.verify(event, Mockito.times(1)).respondWith(captor.capture())
+ assertThat(captor.value).contains("ec2-54-234-237-183.compute-1.amazonaws.com")
+ }
+
+ @Test
+ fun lookupUnknownHostname() {
+ val lookup = Lookup()
+ val event = Mockito.mock(GenericMessageEvent::class.java)
+ val captor = ArgumentCaptor.forClass(String::class.java)
+
+ lookup.commandResponse("channel", "lookup", "foobar", event)
+
+ Mockito.verify(event, Mockito.times(1)).respond(captor.capture())
+ assertThat(captor.value).isEqualTo("Unknown host.")
+ }
+
+
+ }
+
+ @Nested
+ @DisplayName("Lookup Tests")
+ inner class LookupTests {
+ @Test
+ @Throws(Exception::class)
+ fun lookupByHostname() {
+ val result = nslookup("apple.com")
+ assertThat(result, "lookup(apple.com)").contains("17.253.144.10")
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun lookupByIpAddress() {
+ val result = nslookup("37.27.52.13")
+ assertThat(result, "lookup(37.27.52.13)").contains("nix4.thauvin.us")
+ }
}
@Test
@Throws(Exception::class)
- fun testWhois() {
+ fun whois() {
val result = whois("17.178.96.59", Lookup.WHOIS_HOST)
assertThat(result, "whois(17.178.96.59").any { it.contains("Apple Inc.") }
}
diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/modules/MastodonTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/modules/MastodonTest.kt
index a9e1d43..d8e511e 100644
--- a/src/test/kotlin/net/thauvin/erik/mobibot/modules/MastodonTest.kt
+++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/MastodonTest.kt
@@ -30,25 +30,104 @@
*/
package net.thauvin.erik.mobibot.modules
+import assertk.assertFailure
import assertk.assertThat
import assertk.assertions.contains
+import assertk.assertions.hasMessage
+import assertk.assertions.isEqualTo
+import assertk.assertions.isInstanceOf
import net.thauvin.erik.mobibot.LocalProperties
import net.thauvin.erik.mobibot.modules.Mastodon.Companion.toot
+import org.junit.jupiter.api.DisplayName
+import org.junit.jupiter.api.Nested
+import org.mockito.ArgumentCaptor
+import org.mockito.Mockito
+import org.mockito.kotlin.whenever
+import org.pircbotx.hooks.types.GenericMessageEvent
import kotlin.test.Test
class MastodonTest : LocalProperties() {
- @Test
- @Throws(ModuleException::class)
- fun testToot() {
- val msg = "Testing Mastodon API from ${getHostName()}"
- assertThat(
- toot(
- getProperty(Mastodon.ACCESS_TOKEN_PROP),
- getProperty(Mastodon.INSTANCE_PROP),
- getProperty(Mastodon.HANDLE_PROP),
- msg,
- true
- )
- ).contains(msg)
+ @Nested
+ @DisplayName("Command Response Tests")
+ inner class CommandResponseTests {
+ @Test
+ fun `API Key is not specified`() {
+ val mastodon = Mastodon()
+ val event = Mockito.mock(GenericMessageEvent::class.java)
+ val captor = ArgumentCaptor.forClass(String::class.java)
+ val user = Mockito.mock(org.pircbotx.User::class.java)
+
+ whenever(event.user).thenReturn(user)
+ whenever(user.nick).thenReturn("mock")
+
+ mastodon.commandResponse("channel", "toot", "This is a test.", event)
+
+ Mockito.verify(event, Mockito.atLeastOnce()).respond(captor.capture())
+ assertThat(captor.value).isEqualTo("The access token is missing.")
+ }
+ }
+
+ @Nested
+ @DisplayName("Toot Tests")
+ inner class TootTests {
+ @Test
+ @Throws(ModuleException::class)
+ fun `Empty Access Token should throw exception`() {
+ val msg = "Testing Mastodon API from ${getHostName()}"
+ assertFailure {
+ toot(
+ "",
+ getProperty(Mastodon.INSTANCE_PROP),
+ getProperty(Mastodon.HANDLE_PROP),
+ msg,
+ true
+ )
+ }.isInstanceOf(ModuleException::class.java).hasMessage("The access token is missing.")
+ }
+
+ @Test
+ @Throws(ModuleException::class)
+ fun `Empty Handle should throw exception`() {
+ val msg = "Testing Mastodon API from ${getHostName()}"
+ assertFailure {
+ toot(
+ getProperty(Mastodon.ACCESS_TOKEN_PROP),
+ getProperty(Mastodon.INSTANCE_PROP),
+ "",
+ msg,
+ true
+ )
+ }.isInstanceOf(ModuleException::class.java).hasMessage("The Mastodon handle is missing.")
+ }
+
+ @Test
+ @Throws(ModuleException::class)
+ fun `Empty Instance should throw exception`() {
+ val msg = "Testing Mastodon API from ${getHostName()}"
+ assertFailure {
+ toot(
+ getProperty(Mastodon.ACCESS_TOKEN_PROP),
+ "",
+ getProperty(Mastodon.HANDLE_PROP),
+ msg,
+ true
+ )
+ }.isInstanceOf(ModuleException::class.java).hasMessage("The Mastodon instance is missing.")
+ }
+
+ @Test
+ @Throws(ModuleException::class)
+ fun `Toot on Mastodon`() {
+ val msg = "Testing Mastodon API from ${getHostName()}"
+ assertThat(
+ toot(
+ getProperty(Mastodon.ACCESS_TOKEN_PROP),
+ getProperty(Mastodon.INSTANCE_PROP),
+ getProperty(Mastodon.HANDLE_PROP),
+ msg,
+ true
+ )
+ ).contains(msg)
+ }
}
}
diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/modules/ModuleExceptionTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/modules/ModuleExceptionTest.kt
index 6c3c54c..87de5dd 100644
--- a/src/test/kotlin/net/thauvin/erik/mobibot/modules/ModuleExceptionTest.kt
+++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/ModuleExceptionTest.kt
@@ -34,6 +34,8 @@ import assertk.all
import assertk.assertThat
import assertk.assertions.*
import net.thauvin.erik.mobibot.ExceptionSanitizer.sanitize
+import org.junit.jupiter.api.DisplayName
+import org.junit.jupiter.api.Nested
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.MethodSource
import java.io.IOException
@@ -45,60 +47,64 @@ class ModuleExceptionTest {
const val MESSAGE = "message"
@JvmStatic
- fun dataProviders(): List {
+ fun moduleExceptions(): List {
return listOf(
- ModuleException(DEBUG_MESSAGE, MESSAGE, IOException("URL http://foobar.com")),
- ModuleException(DEBUG_MESSAGE, MESSAGE, IOException("URL http://foobar.com?")),
+ ModuleException(DEBUG_MESSAGE, MESSAGE, IOException("foo")),
+ ModuleException(DEBUG_MESSAGE, MESSAGE, IllegalArgumentException("bar")),
ModuleException(DEBUG_MESSAGE, MESSAGE)
)
}
}
- @ParameterizedTest
- @MethodSource("dataProviders")
- fun testGetDebugMessage(e: ModuleException) {
- assertThat(e::debugMessage).isEqualTo(DEBUG_MESSAGE)
- }
-
- @ParameterizedTest
- @MethodSource("dataProviders")
- fun testGetMessage(e: ModuleException) {
- assertThat(e).hasMessage(MESSAGE)
- }
-
- @Test
- fun testSanitizeMessage() {
- val apiKey = "1234567890"
- var e = ModuleException(DEBUG_MESSAGE, MESSAGE, IOException("URL http://foo.com?apiKey=$apiKey&userID=me"))
- assertThat(
- e.sanitize(apiKey, "", "me").message, "ModuleException(debugMessage, message, IOException(url))"
- ).isNotNull().all {
- contains("xxxxxxxxxx", "userID=xx", "java.io.IOException")
- doesNotContain(apiKey, "me")
+ @Nested
+ @DisplayName("Message Tests")
+ inner class MessageTests {
+ @ParameterizedTest
+ @MethodSource("net.thauvin.erik.mobibot.modules.ModuleExceptionTest#moduleExceptions")
+ fun getDebugMessage(e: ModuleException) {
+ assertThat(e::debugMessage).isEqualTo(DEBUG_MESSAGE)
}
- e = ModuleException(DEBUG_MESSAGE, MESSAGE, null)
- assertThat(e.sanitize(apiKey), "ModuleException(debugMessage, message, null)").hasMessage(MESSAGE)
-
- e = ModuleException(DEBUG_MESSAGE, MESSAGE, IOException())
- assertThat(e.sanitize(apiKey), "ModuleException(debugMessage, message, IOException())").hasMessage(MESSAGE)
-
- e = ModuleException(DEBUG_MESSAGE, apiKey)
- assertThat(e.sanitize(apiKey).message, "ModuleException(debugMessage, apiKey)").isNotNull()
- .doesNotContain(apiKey)
-
- val msg: String? = null
- e = ModuleException(DEBUG_MESSAGE, msg, IOException(msg))
- assertThat(e.sanitize(apiKey).message, "ModuleException(debugMessage, msg, IOException(msg))").isNull()
-
- e = ModuleException(DEBUG_MESSAGE, msg, IOException("foo is $apiKey"))
- assertThat(
- e.sanitize(" ", apiKey, "foo").message,
- "ModuleException(debugMessage, msg, IOException(foo is $apiKey))"
- ).isNotNull().all {
- doesNotContain(apiKey)
- endsWith("xxx is xxxxxxxxxx")
+ @ParameterizedTest
+ @MethodSource("net.thauvin.erik.mobibot.modules.ModuleExceptionTest#moduleExceptions")
+ fun getMessage(e: ModuleException) {
+ assertThat(e).hasMessage(MESSAGE)
+ }
+
+ @Test
+ fun sanitizeMessage() {
+ val apiKey = "1234567890"
+ var e = ModuleException(DEBUG_MESSAGE, MESSAGE, IOException("URL https://foo.com?apiKey=$apiKey&userID=me"))
+ assertThat(
+ e.sanitize(apiKey, "", "me").message, "ModuleException(debugMessage, message, IOException(url))"
+ ).isNotNull().all {
+ contains("xxxxxxxxxx", "userID=xx", "java.io.IOException")
+ doesNotContain(apiKey, "me")
+ }
+
+ e = ModuleException(DEBUG_MESSAGE, MESSAGE, null)
+ assertThat(e.sanitize(apiKey), "ModuleException(debugMessage, message, null)").hasMessage(MESSAGE)
+
+ e = ModuleException(DEBUG_MESSAGE, MESSAGE, IOException())
+ assertThat(e.sanitize(apiKey), "ModuleException(debugMessage, message, IOException())").hasMessage(MESSAGE)
+
+ e = ModuleException(DEBUG_MESSAGE, apiKey)
+ assertThat(e.sanitize(apiKey).message, "ModuleException(debugMessage, apiKey)").isNotNull()
+ .doesNotContain(apiKey)
+
+ val msg: String? = null
+ e = ModuleException(DEBUG_MESSAGE, msg, IOException(msg))
+ assertThat(e.sanitize(apiKey).message, "ModuleException(debugMessage, msg, IOException(msg))").isNull()
+
+ e = ModuleException(DEBUG_MESSAGE, msg, IOException("foo is $apiKey"))
+ assertThat(
+ e.sanitize(" ", apiKey, "foo").message,
+ "ModuleException(debugMessage, msg, IOException(foo is $apiKey))"
+ ).isNotNull().all {
+ doesNotContain(apiKey)
+ endsWith("xxx is xxxxxxxxxx")
+ }
+ assertThat(e.sanitize(), "exception should be unchanged").isEqualTo(e)
}
- assertThat(e.sanitize(), "exception should be unchanged").isEqualTo(e)
}
}
diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/modules/PingTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/modules/PingTest.kt
index f51e203..cea44a9 100644
--- a/src/test/kotlin/net/thauvin/erik/mobibot/modules/PingTest.kt
+++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/PingTest.kt
@@ -38,14 +38,14 @@ import kotlin.test.Test
class PingTest {
@Test
- fun testPingsArray() {
- assertThat(Ping.PINGS, "Ping.PINGS").isNotEmpty()
- }
-
- @Test
- fun testRandomPing() {
+ fun `Get a random ping`() {
for (i in 0..9) {
assertThat(Ping.PINGS, "Ping.PINGS[$i]").contains(randomPing())
}
}
+
+ @Test
+ fun `Pings array should not be empty`() {
+ assertThat(Ping.PINGS, "Ping.PINGS").isNotEmpty()
+ }
}
diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/modules/RockPaperScissorsTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/modules/RockPaperScissorsTest.kt
index 519037a..59b0a29 100644
--- a/src/test/kotlin/net/thauvin/erik/mobibot/modules/RockPaperScissorsTest.kt
+++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/RockPaperScissorsTest.kt
@@ -33,18 +33,82 @@ package net.thauvin.erik.mobibot.modules
import assertk.assertThat
import assertk.assertions.isEqualTo
+import assertk.assertions.matches
import net.thauvin.erik.mobibot.modules.RockPaperScissors.Companion.winLoseOrDraw
+import org.junit.jupiter.api.DisplayName
+import org.junit.jupiter.api.Nested
+import org.junit.jupiter.api.RepeatedTest
+import org.mockito.ArgumentCaptor
+import org.mockito.Mockito
+import org.pircbotx.hooks.types.GenericMessageEvent
import kotlin.test.Test
class RockPaperScissorsTest {
- @Test
- fun testWinLoseOrDraw() {
- assertThat(winLoseOrDraw("scissors", "paper"), "scissors vs. paper").isEqualTo("win")
- assertThat(winLoseOrDraw("paper", "rock"), "paper vs. rock").isEqualTo("win")
- assertThat(winLoseOrDraw("rock", "scissors"), "rock vs. scissors").isEqualTo("win")
- assertThat(winLoseOrDraw("paper", "scissors"), "paper vs. scissors").isEqualTo("lose")
- assertThat(winLoseOrDraw("rock", "paper"), "rock vs. paper").isEqualTo("lose")
- assertThat(winLoseOrDraw("scissors", "rock"), "scissors vs. rock").isEqualTo("lose")
- assertThat(winLoseOrDraw("scissors", "scissors"), "scissors vs. scissors").isEqualTo("draw")
+ @Nested
+ @DisplayName("Command Response Tests")
+ inner class CommandResponseTests {
+ @RepeatedTest(3)
+ fun `Play Rock Paper Scissors`() {
+ val rockPaperScissors = RockPaperScissors()
+ val event = Mockito.mock(GenericMessageEvent::class.java)
+ val captor = ArgumentCaptor.forClass(String::class.java)
+
+ rockPaperScissors.commandResponse("channel", "rock", "", event)
+
+ Mockito.verify(event, Mockito.atLeastOnce()).respond(captor.capture())
+ assertThat(captor.value)
+ .matches(
+ ".* (vs\\.|crushes|covers|cuts) (ROCK|PAPER|SCISSORS) » You \u0002(tie|win|lose)\u0002.".toRegex()
+ )
+ }
+ }
+
+ @Nested
+ @DisplayName("Win, Lose or Draw Tests")
+ inner class WinLoseOrDrawTests {
+ @Test
+ fun `Paper versus Paper draws`() {
+ assertThat(winLoseOrDraw("paper", "paper")).isEqualTo("draw")
+ }
+
+ @Test
+ fun `Paper versus Rock wins`() {
+ assertThat(winLoseOrDraw("paper", "rock")).isEqualTo("win")
+ }
+
+ @Test
+ fun `Paper versus Scissors loses`() {
+ assertThat(winLoseOrDraw("paper", "scissors")).isEqualTo("lose")
+ }
+
+ @Test
+ fun `Rock versus Paper loses`() {
+ assertThat(winLoseOrDraw("rock", "paper")).isEqualTo("lose")
+ }
+
+ @Test
+ fun `Rock versus Rock draws`() {
+ assertThat(winLoseOrDraw("rock", "rock")).isEqualTo("draw")
+ }
+
+ @Test
+ fun `Rock versus Scissors wins`() {
+ assertThat(winLoseOrDraw("rock", "scissors")).isEqualTo("win")
+ }
+
+ @Test
+ fun `Scissors versus Paper wins`() {
+ assertThat(winLoseOrDraw("scissors", "paper")).isEqualTo("win")
+ }
+
+ @Test
+ fun `Scissors versus Rock loses`() {
+ assertThat(winLoseOrDraw("scissors", "rock")).isEqualTo("lose")
+ }
+
+ @Test
+ fun `Scissors versus Scissors draws`() {
+ assertThat(winLoseOrDraw("scissors", "scissors")).isEqualTo("draw")
+ }
}
}
diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/modules/StockQuoteTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/modules/StockQuoteTest.kt
index 955a267..8f321c2 100644
--- a/src/test/kotlin/net/thauvin/erik/mobibot/modules/StockQuoteTest.kt
+++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/StockQuoteTest.kt
@@ -39,37 +39,23 @@ import net.thauvin.erik.mobibot.LocalProperties
import net.thauvin.erik.mobibot.modules.StockQuote.Companion.getQuote
import net.thauvin.erik.mobibot.msg.ErrorMessage
import net.thauvin.erik.mobibot.msg.Message
+import org.junit.jupiter.api.DisplayName
+import org.junit.jupiter.api.Nested
+import org.mockito.ArgumentCaptor
+import org.mockito.Mockito
+import org.pircbotx.hooks.types.GenericMessageEvent
import kotlin.test.Test
class StockQuoteTest : LocalProperties() {
+ private val apiKey = getProperty(StockQuote.API_KEY_PROP)
+
private fun buildMatch(label: String): String {
return "${label}:[ ]+[0-9.]+".prependIndent()
}
- @Test
- @Throws(ModuleException::class)
- fun testGetQuote() {
- val apiKey = getProperty(StockQuote.API_KEY_PROP)
+ private fun getSanitizedQuote(symbol: String, apiKey: String): List {
try {
- var symbol = "apple inc"
- val messages = getQuote(symbol, apiKey)
- assertThat(messages, "response not empty").isNotEmpty()
- assertThat(messages, "getQuote($symbol)").index(0).prop(Message::msg).matches("Symbol: AAPL .*".toRegex())
- assertThat(messages, "getQuote($symbol)").index(1).prop(Message::msg).matches(buildMatch("Price").toRegex())
- assertThat(messages, "getQuote($symbol)").index(2).prop(Message::msg)
- .matches(buildMatch("Previous").toRegex())
- assertThat(messages, "getQuote($symbol)").index(3).prop(Message::msg).matches(buildMatch("Open").toRegex())
-
- symbol = "blahfoo"
- assertThat(getQuote(symbol, apiKey).first(), "getQuote($symbol)").all {
- isInstanceOf(ErrorMessage::class.java)
- prop(Message::msg).isEqualTo(StockQuote.INVALID_SYMBOL)
- }
- assertThat(getQuote("", "apikey").first(), "getQuote(empty)").all {
- isInstanceOf(ErrorMessage::class.java)
- prop(Message::msg).isEqualTo(StockQuote.INVALID_SYMBOL)
- }
- assertFailure { getQuote("test", "") }.isInstanceOf(ModuleException::class.java).hasNoCause()
+ return getQuote(symbol, apiKey)
} catch (e: ModuleException) {
// Avoid displaying api keys in CI logs
if ("true" == System.getenv("CI")) {
@@ -79,4 +65,76 @@ class StockQuoteTest : LocalProperties() {
}
}
}
+
+ @Nested
+ @DisplayName("Command Response Tests")
+ inner class CommandResponseTests {
+ @Test
+ fun `API Key is missing`() {
+ val stockQuote = StockQuote()
+ val event = Mockito.mock(GenericMessageEvent::class.java)
+ val captor = ArgumentCaptor.forClass(String::class.java)
+
+ stockQuote.commandResponse("channel", "stock", "goog", event)
+
+ Mockito.verify(event, Mockito.atLeastOnce()).respond(captor.capture())
+ assertThat(captor.value).isEqualTo("${StockQuote.SERVICE_NAME} is disabled. The API key is missing.")
+ }
+ }
+
+ @Nested
+ @DisplayName("Get Quote Tests")
+ inner class GetQuoteTests {
+ @Test
+ @Throws(ModuleException::class)
+ fun `API key should not be empty`() {
+ assertFailure { getSanitizedQuote("test", "") }.isInstanceOf(ModuleException::class.java)
+ .messageContains("disabled")
+ }
+
+ @Test
+ @Throws(ModuleException::class)
+ fun `Symbol should not be empty`() {
+ assertThat(getSanitizedQuote("", "apikey").first(), "getQuote(empty)").all {
+ isInstanceOf(ErrorMessage::class.java)
+ prop(Message::msg).isEqualTo(StockQuote.INVALID_SYMBOL)
+ }
+ }
+
+ @Test
+ @Throws(ModuleException::class)
+ fun `Get stock quote for Apple`() {
+ val symbol = "apple inc"
+ val messages = getSanitizedQuote(symbol, apiKey)
+ assertThat(messages, "response not empty").isNotEmpty()
+ assertThat(messages, "getQuote($symbol)").index(0).prop(Message::msg)
+ .matches("Symbol: AAPL .*".toRegex())
+ assertThat(messages, "getQuote($symbol)").index(1).prop(Message::msg)
+ .matches(buildMatch("Price").toRegex())
+ assertThat(messages, "getQuote($symbol)").index(2).prop(Message::msg)
+ .matches(buildMatch("Previous").toRegex())
+ assertThat(messages, "getQuote($symbol)").index(3).prop(Message::msg)
+ .matches(buildMatch("Open").toRegex())
+ }
+
+ @Test
+ @Throws(ModuleException::class)
+ fun `Get stock quote for Google`() {
+ val symbol = "goog"
+ val messages = getSanitizedQuote(symbol, apiKey)
+ assertThat(messages, "response not empty").isNotEmpty()
+ assertThat(messages, "getQuote($symbol)").index(0).prop(Message::msg)
+ .matches("Symbol: GOOG .*".toRegex())
+ }
+
+ @Test
+ @Throws(ModuleException::class)
+ fun `Invalid symbol should throw exception`() {
+ val symbol = "foobar"
+ assertThat(getSanitizedQuote(symbol, apiKey).first(), "getQuote($symbol)").all {
+ isInstanceOf(ErrorMessage::class.java)
+ prop(Message::msg).isEqualTo(StockQuote.INVALID_SYMBOL)
+ }
+ }
+ }
}
diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/modules/WarTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/modules/WarTest.kt
new file mode 100644
index 0000000..35a53b1
--- /dev/null
+++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/WarTest.kt
@@ -0,0 +1,54 @@
+/*
+ * WarTest.kt
+ *
+ * Copyright 2004-2025 Erik C. Thauvin (erik@thauvin.net)
+ *
+ * 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 assertk.assertThat
+import assertk.assertions.matches
+import org.junit.jupiter.api.RepeatedTest
+import org.mockito.ArgumentCaptor
+import org.mockito.Mockito
+import org.pircbotx.hooks.types.GenericMessageEvent
+
+class WarTest {
+ @RepeatedTest(3)
+ fun `Play war`() {
+ val war = War()
+ val event = Mockito.mock(GenericMessageEvent::class.java)
+ val captor = ArgumentCaptor.forClass(String::class.java)
+
+ war.commandResponse("channel", "war", "", event)
+
+ Mockito.verify(event, Mockito.atLeastOnce()).respond(captor.capture())
+ assertThat(captor.value)
+ .matches("[\uD83C\uDCA0-\uD83C\uDCDF] {2}[\uD83C\uDCA0-\uD83C\uDCDF] {2}» .+(win|lose|tie).+!".toRegex())
+ }
+}
diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/modules/Weather2Test.kt b/src/test/kotlin/net/thauvin/erik/mobibot/modules/Weather2Test.kt
index 5d04560..343165c 100644
--- a/src/test/kotlin/net/thauvin/erik/mobibot/modules/Weather2Test.kt
+++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/Weather2Test.kt
@@ -43,72 +43,141 @@ import net.thauvin.erik.mobibot.modules.Weather2.Companion.getCountry
import net.thauvin.erik.mobibot.modules.Weather2.Companion.getWeather
import net.thauvin.erik.mobibot.modules.Weather2.Companion.mphToKmh
import net.thauvin.erik.mobibot.msg.Message
+import org.junit.jupiter.api.DisplayName
+import org.junit.jupiter.api.Nested
+import org.mockito.ArgumentCaptor
+import org.mockito.Mockito
+import org.pircbotx.hooks.types.GenericMessageEvent
import kotlin.test.Test
class Weather2Test : LocalProperties() {
- @Test
- fun testFtoC() {
- val t = ftoC(32.0)
- assertThat(t.second, "32 °F is 0 °C").isEqualTo(0)
- }
+ @Nested
+ @DisplayName("Command Response Tests")
+ inner class CommandResponseTests {
+ @Test
+ fun `API Key is missing`() {
+ val weather2 = Weather2()
+ val event = Mockito.mock(GenericMessageEvent::class.java)
+ val captor = ArgumentCaptor.forClass(String::class.java)
- @Test
- fun testGetCountry() {
- assertThat(getCountry("foo"), "foo is not a valid country").isEqualTo(OWM.Country.UNITED_STATES)
- assertThat(getCountry("fr"), "country should France").isEqualTo(OWM.Country.FRANCE)
+ weather2.commandResponse("channel", "weather", "seattle, us", event)
- val country = OWM.Country.entries.toTypedArray()
- repeat(3) {
- val rand = country[(country.indices).random()]
- assertThat(getCountry(rand.value), rand.name).isEqualTo(rand)
+ Mockito.verify(event, Mockito.atLeastOnce()).respond(captor.capture())
+ assertThat(captor.value).isEqualTo("${Weather2.WEATHER_NAME} is disabled. The API key is missing.")
}
}
- @Test
- fun testMphToKmh() {
- val w = mphToKmh(0.62)
- assertThat(w.second, "0.62 mph is 1 km/h").isEqualTo(1)
+ @Nested
+ @DisplayName("API Key Tests")
+ inner class ApiKeyTests {
+ @Test
+ @Throws(ModuleException::class)
+ fun `API Key should not be empty`() {
+ assertFailure { getWeather("test", "") }.isInstanceOf(ModuleException::class.java).hasNoCause()
+ }
+
+ @Test
+ @Throws(ModuleException::class)
+ fun `API Key should not be null`() {
+ assertFailure { getWeather("test", null) }.isInstanceOf(ModuleException::class.java).hasNoCause()
+ }
}
- @Test
- @Throws(ModuleException::class)
- fun testWeather() {
- var query = "98204"
- var messages = getWeather(query, getProperty(API_KEY_PROP))
- assertThat(messages, "getWeather($query)").index(0).prop(Message::msg).all {
- contains("Everett, United States")
- contains("US")
- }
- assertThat(messages, "getWeather($query)").index(messages.size - 1).prop(Message::msg).endsWith("98204%2CUS")
-
- query = "San Francisco"
- messages = getWeather(query, getProperty(API_KEY_PROP))
- assertThat(messages, "getWeather($query)").index(0).prop(Message::msg).all {
- contains("San Francisco")
- contains("US")
- }
- assertThat(messages, "getWeather($query)").index(messages.size - 1).prop(Message::msg).endsWith("5391959")
-
- query = "London, GB"
- messages = getWeather(query, getProperty(API_KEY_PROP))
- assertThat(messages, "getWeather($query)").index(0).prop(Message::msg).all {
- contains("London, United Kingdom")
- contains("GB")
- }
- assertThat(messages, "getWeather($query)").index(messages.size - 1).prop(Message::msg).endsWith("2643743")
-
- try {
- query = "Foo, US"
- getWeather(query, getProperty(API_KEY_PROP))
- } catch (e: ModuleException) {
- assertThat(e.cause, "getWeather($query)").isNotNull().isInstanceOf(APIException::class.java)
+ @Nested
+ @DisplayName("Conversion Tests")
+ inner class ConversionTests {
+ @Test
+ fun `Fahrenheit to Centigrade`() {
+ val t = ftoC(32.0)
+ assertThat(t.second, "32 °F is 0 °C").isEqualTo(0)
}
- query = "test"
- assertFailure { getWeather(query, "") }.isInstanceOf(ModuleException::class.java).hasNoCause()
- assertFailure { getWeather(query, null) }.isInstanceOf(ModuleException::class.java).hasNoCause()
+ @Test
+ fun `Miles per hour to Kilometers per hour`() {
+ val w = mphToKmh(0.62)
+ assertThat(w.second, "0.62 mph is 1 km/h").isEqualTo(1)
+ }
+ }
- messages = getWeather("", "apikey")
- assertThat(messages, "getWeather(empty)").index(0).prop(Message::isError).isTrue()
+ @Nested
+ @DisplayName("Country Tests")
+ inner class CountryTests {
+ @Test
+ fun `Foo is not a valid country`() {
+ assertThat(getCountry("foo")).isEqualTo(OWM.Country.UNITED_STATES)
+ }
+
+ @Test
+ fun `FR is France`() {
+ assertThat(getCountry("fr")).isEqualTo(OWM.Country.FRANCE)
+ }
+
+ @Test
+ fun `Get random country`() {
+ val country = OWM.Country.entries.toTypedArray()
+ repeat(3) {
+ val rand = country[(country.indices).random()]
+ assertThat(getCountry(rand.value), rand.name).isEqualTo(rand)
+ }
+ }
+ }
+
+ @Nested
+ @DisplayName("Weather Tests")
+ inner class WeatherTests {
+ @Test
+ @Throws(ModuleException::class)
+ fun `Empty query`() {
+ val messages = getWeather("", "apikey")
+ assertThat(messages, "getWeather(empty)").index(0).prop(Message::isError).isTrue()
+ }
+
+ @Test
+ @Throws(ModuleException::class)
+ fun `Everett, Zip Code 98204`() {
+ val query = "98204"
+ val messages = getWeather(query, getProperty(API_KEY_PROP))
+ assertThat(messages, "getWeather($query)").index(0).prop(Message::msg).all {
+ contains("Everett, United States")
+ contains("US")
+ }
+ assertThat(messages, "getWeather($query)").index(messages.size - 1).prop(Message::msg)
+ .endsWith("98204%2CUS")
+ }
+
+ @Test
+ @Throws(ModuleException::class)
+ fun `Foo, US`() {
+ val query = "Foo, US"
+ try {
+ getWeather(query, getProperty(API_KEY_PROP))
+ } catch (e: ModuleException) {
+ assertThat(e.cause, "getWeather($query)").isNotNull().isInstanceOf(APIException::class.java)
+ }
+ }
+
+ @Test
+ @Throws(ModuleException::class)
+ fun `London, GB`() {
+ val query = "London, GB"
+ val messages = getWeather(query, getProperty(API_KEY_PROP))
+ assertThat(messages, "getWeather($query)").index(0).prop(Message::msg).all {
+ contains("London, United Kingdom")
+ contains("GB")
+ }
+ assertThat(messages, "getWeather($query)").index(messages.size - 1).prop(Message::msg).endsWith("2643743")
+ }
+
+ @Test
+ @Throws(ModuleException::class)
+ fun `San Francisco`() {
+ val query = "San Francisco"
+ val messages = getWeather(query, getProperty(API_KEY_PROP))
+ assertThat(messages, "getWeather($query)").index(0).prop(Message::msg).all {
+ contains("San Francisco")
+ contains("US")
+ }
+ assertThat(messages, "getWeather($query)").index(messages.size - 1).prop(Message::msg).endsWith("5391959")
+ }
}
}
diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/modules/WolframAlphaTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/modules/WolframAlphaTest.kt
index 099f3f9..764b446 100644
--- a/src/test/kotlin/net/thauvin/erik/mobibot/modules/WolframAlphaTest.kt
+++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/WolframAlphaTest.kt
@@ -35,38 +35,81 @@ import assertk.assertFailure
import assertk.assertThat
import assertk.assertions.contains
import assertk.assertions.hasMessage
+import assertk.assertions.isEqualTo
import assertk.assertions.isInstanceOf
import net.thauvin.erik.mobibot.DisableOnCi
import net.thauvin.erik.mobibot.ExceptionSanitizer.sanitize
import net.thauvin.erik.mobibot.LocalProperties
import net.thauvin.erik.mobibot.modules.WolframAlpha.Companion.queryWolfram
+import org.junit.jupiter.api.DisplayName
+import org.junit.jupiter.api.Nested
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.Arguments
+import org.junit.jupiter.params.provider.MethodSource
+import org.mockito.ArgumentCaptor
+import org.mockito.Mockito
+import org.pircbotx.hooks.types.GenericMessageEvent
+import java.util.stream.Stream
import kotlin.test.Test
class WolframAlphaTest : LocalProperties() {
- @Test
- fun testAppId() {
- assertFailure { queryWolfram("1 gallon to liter", appId = "DEMO") }
- .isInstanceOf(ModuleException::class.java)
- .hasMessage("Error 1: Invalid appid")
-
- assertFailure { queryWolfram("1 gallon to liter", appId = "") }
- .isInstanceOf(ModuleException::class.java)
+ companion object {
+ @JvmStatic
+ fun queries(): Stream {
+ return Stream.of(
+ Arguments.of("SFO to SEA", "", "miles"),
+ Arguments.of("SFO to LAS", WolframAlpha.IMPERIAL, "miles"),
+ Arguments.of("SFO to LAX", WolframAlpha.METRIC, "kilometers")
+ )
+ }
}
- @Test
+ @Nested
+ @DisplayName("Command Response Tests")
+ inner class CommandResponseTests {
+ @Test
+ fun appIdNotSpecified() {
+ val wolframAlpha = WolframAlpha()
+ val event = Mockito.mock(GenericMessageEvent::class.java)
+ val captor = ArgumentCaptor.forClass(String::class.java)
+
+ wolframAlpha.commandResponse("channel", "wolfram", "1 liter to gallon", event)
+
+ Mockito.verify(event, Mockito.atLeastOnce()).respond(captor.capture())
+ assertThat(captor.value).isEqualTo("No ${WolframAlpha.SERVICE_NAME} AppID specified.")
+ }
+ }
+
+ @Nested
+ @DisplayName("App ID Tests")
+ inner class AppIdTests {
+ @Test
+ fun emptyAppId() {
+ assertFailure { queryWolfram("1 gallon to liter", appId = "") }
+ .isInstanceOf(ModuleException::class.java)
+ }
+
+ @Test
+ fun invalidAppId() {
+ assertFailure { queryWolfram("1 gallon to liter", appId = "DEMO") }
+ .isInstanceOf(ModuleException::class.java)
+ .hasMessage("Error 1: Invalid appid")
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("queries")
@DisableOnCi
@Throws(ModuleException::class)
- fun queryWolframTest() {
+ fun queryWolfram(query: String, units: String, expected: String) {
val apiKey = getProperty(WolframAlpha.APPID_KEY_PROP)
try {
- var query = "SFO to SEA"
- assertThat(queryWolfram(query, appId = apiKey), "queryWolfram($query)").contains("miles")
-
- query = "SFO to LAX"
- assertThat(
- queryWolfram(query, WolframAlpha.METRIC, apiKey),
- "queryWolfram($query)"
- ).contains("kilometers")
+ if (units.isBlank()) {
+ assertThat(queryWolfram(query, appId = apiKey), "queryWolfram($query)").contains(expected)
+ } else {
+ assertThat(queryWolfram(query, units, appId = apiKey), "queryWolfram($query, $units)")
+ .contains(expected)
+ }
} catch (e: ModuleException) {
// Avoid displaying api key in CI logs
if ("true" == System.getenv("CI")) {
diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/modules/WordTimeTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/modules/WordTimeTest.kt
index 396efaf..e85b5a5 100644
--- a/src/test/kotlin/net/thauvin/erik/mobibot/modules/WordTimeTest.kt
+++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/WordTimeTest.kt
@@ -38,30 +38,80 @@ import net.thauvin.erik.mobibot.Utils.bold
import net.thauvin.erik.mobibot.modules.WorldTime.Companion.BEATS_KEYWORD
import net.thauvin.erik.mobibot.modules.WorldTime.Companion.COUNTRIES_MAP
import net.thauvin.erik.mobibot.modules.WorldTime.Companion.time
+import org.junit.jupiter.api.DisplayName
+import org.junit.jupiter.api.Nested
+import org.mockito.ArgumentCaptor
+import org.mockito.Mockito
import org.pircbotx.Colors
+import org.pircbotx.hooks.types.GenericMessageEvent
import java.time.ZoneId
import kotlin.test.Test
class WordTimeTest {
- @Test
- fun testTime() {
- assertThat(time(), "time()").matches(
- ("The time is ${Colors.BOLD}\\d{1,2}:\\d{2}${Colors.BOLD} " +
- "on ${Colors.BOLD}\\w+, \\d{1,2} \\w+ \\d{4}${Colors.BOLD} " +
- "in ${Colors.BOLD}Los Angeles${Colors.BOLD}").toRegex()
- )
- assertThat(time(""), "time()").endsWith("Los Angeles".bold())
- assertThat(time("PST"), "time(PST)").endsWith("Los Angeles".bold())
- assertThat(time("GB"), "time(GB)").endsWith("London".bold())
- assertThat(time("FR"), "time(FR)").endsWith("Paris".bold())
- assertThat(time("BLAH"), "time(BLAH)").startsWith("Unsupported")
- assertThat(time("BEAT"), "time($BEATS_KEYWORD)").matches("[\\w ]+ .?@\\d{3}+.? .beats".toRegex())
+ @Nested
+ @DisplayName("Command Response Tests")
+ inner class CommandResponseTests {
+ @Test
+ fun `Time in Tokyo`() {
+ val worldTime = WorldTime()
+ val event = Mockito.mock(GenericMessageEvent::class.java)
+ val captor = ArgumentCaptor.forClass(String::class.java)
+
+ worldTime.commandResponse("channel", "time", "jp", event)
+
+ Mockito.verify(event, Mockito.atLeastOnce()).respond(captor.capture())
+ assertThat(captor.value)
+ .matches("The time is \u0002([01]\\d|2[0-3]):([0-5]\\d)\u0002 on .+ in \u0002Tokyo\u0002".toRegex())
+ }
+ }
+
+ @Nested
+ @DisplayName("Time Tests")
+ inner class TimeTests {
+ @Test
+ fun `Check default time formatting`() {
+ assertThat(time(), "time()").matches(
+ ("The time is ${Colors.BOLD}\\d{1,2}:\\d{2}${Colors.BOLD} " +
+ "on ${Colors.BOLD}\\w+, \\d{1,2} \\w+ \\d{4}${Colors.BOLD} " +
+ "in ${Colors.BOLD}Los Angeles${Colors.BOLD}").toRegex()
+ )
+ }
+
+ @Test
+ fun `Time in Los Angeles`() {
+ assertThat(time(""), "time()").endsWith("Los Angeles".bold())
+ }
+
+ @Test
+ fun `Pacific Standard Time`() {
+ assertThat(time("PST"), "time(PST)").endsWith("Los Angeles".bold())
+ }
+
+ @Test
+ fun `Time in Great Britain`() {
+ assertThat(time("GB"), "time(GB)").endsWith("London".bold())
+ }
+
+ @Test
+ fun `Time in France`() {
+ assertThat(time("FR"), "time(FR)").endsWith("Paris".bold())
+ }
+
+ @Test
+ fun `Time in Unknown Country`() {
+ assertThat(time("BLAH"), "time(BLAH)").startsWith("Unsupported")
+ }
+
+ @Test
+ fun `Swatch Internet Time`() {
+ assertThat(time("BEAT"), "time($BEATS_KEYWORD)").matches("[\\w ]+ .?@\\d{3}+.? .beats".toRegex())
+ }
}
@Test
- fun testZones() {
+ fun `Check that all countries have a valid ZoneId`() {
COUNTRIES_MAP.filter { it.value != BEATS_KEYWORD }.forEach {
- assertThat(ZoneId.of(it.value))
+ assertThat(ZoneId.of(it.value), "ZoneId(${it.value})")
}
}
}
diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/msg/MessageTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/msg/MessageTest.kt
index 6e85ed1..afef54e 100644
--- a/src/test/kotlin/net/thauvin/erik/mobibot/msg/MessageTest.kt
+++ b/src/test/kotlin/net/thauvin/erik/mobibot/msg/MessageTest.kt
@@ -36,74 +36,88 @@ import assertk.assertThat
import assertk.assertions.isFalse
import assertk.assertions.isTrue
import assertk.assertions.prop
+import org.junit.jupiter.api.DisplayName
+import org.junit.jupiter.api.Nested
import kotlin.test.Test
class MessageTest {
- @Test
- fun testConstructor() {
- var msg = Message("foo")
+ @Nested
+ @DisplayName("Constructor Tests")
+ inner class ConstructorTests {
+ @Test
+ fun `Message with error property should be a notice`() {
+ val msg = Message("foo").apply {
+ isError = true
+ }
+ assertThat(msg.isNotice).isTrue()
+ }
- msg.isError = true
- assertThat(msg.isNotice, "message is notice").isTrue()
-
- msg = Message("foo", isError = true)
- assertThat(msg.isNotice, "message is notice too").isTrue()
- }
-
- @Test
- fun testErrorMessage() {
- val msg = ErrorMessage("foo")
- assertThat(msg).all {
- prop(Message::isError).isTrue()
- prop(Message::isNotice).isTrue()
- prop(Message::isPrivate).isFalse()
+ @Test
+ fun `Message with error should be a notice`() {
+ val msg = Message("foo", isError = true)
+ assertThat(msg.isNotice).isTrue()
}
}
- @Test
- fun testIsError() {
- val msg = Message("foo")
- msg.isError = true
- assertThat(msg).all {
- prop(Message::isError).isTrue()
- prop(Message::isNotice).isTrue()
- prop(Message::isPrivate).isFalse()
+ @Nested
+ @DisplayName("Error Tests")
+ inner class ErrorTests {
+ @Test
+ fun `Validate message with error`() {
+ assertThat(Message("foo").apply { isError = true }).all {
+ prop(Message::isError).isTrue()
+ prop(Message::isNotice).isTrue()
+ prop(Message::isPrivate).isFalse()
+ }
}
- msg.isError = false
- assertThat(msg).all {
- prop(Message::isError).isFalse()
- prop(Message::isNotice).isTrue()
- prop(Message::isPrivate).isFalse()
+
+ @Test
+ fun `Validate message without error`() {
+ assertThat(Message("foo").apply { isError = false }).all {
+ prop(Message::isError).isFalse()
+ prop(Message::isNotice).isFalse()
+ prop(Message::isPrivate).isFalse()
+ }
}
}
- @Test
- fun testNoticeMessage() {
- val msg = NoticeMessage("food")
- assertThat(msg).all {
- prop(Message::isError).isFalse()
- prop(Message::isNotice).isTrue()
- prop(Message::isPrivate).isFalse()
+ @Nested
+ @DisplayName("Validate Tests")
+ inner class ValidateTests {
+ @Test
+ fun `Validate error message`() {
+ assertThat(ErrorMessage("foo")).all {
+ prop(Message::isError).isTrue()
+ prop(Message::isNotice).isTrue()
+ prop(Message::isPrivate).isFalse()
+ }
}
- }
- @Test
- fun testPrivateMessage() {
- val msg = PrivateMessage("foo")
- assertThat(msg).all {
- prop(Message::isPrivate).isTrue()
- prop(Message::isError).isFalse()
- prop(Message::isNotice).isFalse()
+ @Test
+ fun `Validate notice message`() {
+ assertThat(NoticeMessage("foo")).all {
+ prop(Message::isError).isFalse()
+ prop(Message::isNotice).isTrue()
+ prop(Message::isPrivate).isFalse()
+ }
}
- }
- @Test
- fun testPublicMessage() {
- val msg = PublicMessage("foo")
- assertThat(msg).all {
- prop(Message::isError).isFalse()
- prop(Message::isNotice).isFalse()
- prop(Message::isPrivate).isFalse()
+ @Test
+ fun `Validate private message`() {
+ assertThat(PrivateMessage("foo")).all {
+ prop(Message::isPrivate).isTrue()
+ prop(Message::isError).isFalse()
+ prop(Message::isNotice).isFalse()
+ }
+ }
+
+ @Test
+ fun `Validate public message`() {
+ assertThat(PublicMessage("foo")).all {
+ prop(Message::isError).isFalse()
+ prop(Message::isNotice).isFalse()
+ prop(Message::isPrivate).isFalse()
+ }
}
}
}