Cleanup tests and KDocs

This commit is contained in:
Erik C. Thauvin 2025-05-05 00:28:06 -07:00
parent b3aab20adf
commit 5bedd323ca
Signed by: erik
GPG key ID: 776702A6A2DA330E
105 changed files with 2523 additions and 1542 deletions

6
.idea/kotlinc.xml generated
View file

@ -7,10 +7,10 @@
<option name="jvmTarget" value="17" />
</component>
<component name="KotlinCommonCompilerArguments">
<option name="apiVersion" value="2.0" />
<option name="languageVersion" value="2.0" />
<option name="apiVersion" value="2.2" />
<option name="languageVersion" value="2.2" />
</component>
<component name="KotlinJpsPluginSettings">
<option name="version" value="2.1.0" />
<option name="version" value="2.1.20" />
</component>
</project>

110
.idea/misc.xml generated
View file

@ -1,12 +1,122 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="EntryPointsManager">
<entry_points version="2.0">
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.modules.Calc name" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.ChannelFeed help" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.links.Comment help" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.Cycle help" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.Die help" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.Info help" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.links.LinksManager help" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.Me help" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.Modules help" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.Msg help" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.Nick help" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.links.Posting help" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.Recap help" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.Say help" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.links.Tags help" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.Users help" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.Versions help" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.links.View help" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.ChannelFeed isOpOnly" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.links.Comment isOpOnly" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.Cycle isOpOnly" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.Die isOpOnly" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.Ignore isOpOnly" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.Info isOpOnly" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.links.LinksManager isOpOnly" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.Me isOpOnly" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.Modules isOpOnly" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.Msg isOpOnly" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.Nick isOpOnly" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.links.Posting isOpOnly" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.Recap isOpOnly" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.Say isOpOnly" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.seen.Seen isOpOnly" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.links.Tags isOpOnly" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.tell.Tell isOpOnly" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.Users isOpOnly" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.Versions isOpOnly" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.links.View isOpOnly" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.modules.WorldTime isPrivateMsgEnabled" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.ChannelFeed isPublic" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.links.Comment isPublic" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.Cycle isPublic" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.Die isPublic" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.Ignore isPublic" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.Info isPublic" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.links.LinksManager isPublic" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.Me isPublic" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.Modules isPublic" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.Msg isPublic" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.Nick isPublic" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.links.Posting isPublic" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.Recap isPublic" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.Say isPublic" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.seen.Seen isPublic" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.links.Tags isPublic" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.tell.Tell isPublic" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.Users isPublic" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.Versions isPublic" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.links.View isPublic" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.ChannelFeed isVisible" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.links.Comment isVisible" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.Cycle isVisible" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.Die isVisible" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.Ignore isVisible" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.Info isVisible" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.links.LinksManager isVisible" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.Me isVisible" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.Modules isVisible" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.Msg isVisible" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.Nick isVisible" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.links.Posting isVisible" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.Recap isVisible" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.Say isVisible" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.seen.Seen isVisible" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.links.Tags isVisible" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.tell.Tell isVisible" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.Users isVisible" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.Versions isVisible" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.links.View isVisible" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.ChannelFeed name" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.links.Comment name" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.modules.CryptoPrices name" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.modules.CurrencyConverter name" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.modules.Dice name" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.Die name" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.modules.GoogleSearch name" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.modules.Joke name" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.links.LinksManager name" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.modules.Lookup name" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.modules.Mastodon name" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.modules.Ping name" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.links.Posting name" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.modules.RockPaperScissors name" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.modules.StockQuote name" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.commands.links.Tags name" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.modules.War name" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.modules.Weather2 name" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.modules.WolframAlpha name" />
<entry_point TYPE="field" FQNAME="net.thauvin.erik.mobibot.modules.WorldTime name" />
</entry_points>
<pattern value="net.thauvin.erik.MobibotBuild" method="deploy" />
<pattern value="net.thauvin.erik.MobibotBuild" method="jacoco" />
<pattern value="net.thauvin.erik.MobibotBuild" />
<pattern value="net.thauvin.erik.MobibotBuild" method="detekt" />
<pattern value="net.thauvin.erik.MobibotBuild" method="detektBaseline" />
<pattern value="net.thauvin.erik.MobibotBuild" method="rootPom" />
<pattern value="net.thauvin.erik.mobibot.Addons" />
<pattern value="net.thauvin.erik.mobibot.Addons" method="match" />
<pattern value="net.thauvin.erik.mobibot.modules.Calc" />
<pattern value="net.thauvin.erik.mobibot.modules.Calc.Companion" />
<pattern value="net.thauvin.erik.mobibot.modules.Mastodon" method="formatEntry" />
<pattern value="net.thauvin.erik.mobibot.Mobibot.Companion" method="main" />
<pattern value="net.thauvin.erik.mobibot.DisableOnCi" />
<pattern value="net.thauvin.erik.mobibot.modules.ModuleExceptionTest.Companion" method="moduleExceptions" />
<pattern value="net.thauvin.erik.mobibot.modules.WolframAlphaTest.Companion" method="queries" />
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" project-jdk-name="17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build" />

View file

@ -1,9 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run Tests" type="Application" factoryName="Application" nameIsGenerated="true">
<option name="MAIN_CLASS_NAME" value="net.thauvin.erik.MobibotTest" />
<module name="app" />
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
</component>

View file

@ -1,6 +1,6 @@
<?xml version="1.0" ?>
<SmellBaseline>
<ManuallySuppressedIssues></ManuallySuppressedIssues>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>CyclomaticComplexMethod:FeedsManager.kt$FeedsManager.Companion$@JvmStatic fun saveFeed(entries: Entries, currentFile: String = CURRENT_XML)</ID>
<ID>CyclomaticComplexMethod:Weather2.kt$Weather2.Companion$@JvmStatic @Throws(ModuleException::class) fun getWeather(query: String, apiKey: String?): List&lt;Message&gt;</ID>
@ -10,8 +10,6 @@
<ID>LongMethod:Weather2.kt$Weather2.Companion$@JvmStatic @Throws(ModuleException::class) fun getWeather(query: String, apiKey: String?): List&lt;Message&gt;</ID>
<ID>LongParameterList:Comment.kt$Comment$( channel: String, cmd: String, entry: EntryLink, entryIndex: Int, commentIndex: Int, event: GenericMessageEvent )</ID>
<ID>LongParameterList:EntryLink.kt$EntryLink$( // Link's comments val comments: MutableList&lt;EntryComment&gt; = mutableListOf(), // Tags/categories val tags: MutableList&lt;SyndCategory&gt; = 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 )</ID>
<ID>MagicNumber:ChatGpt.kt$ChatGpt.Companion$200</ID>
<ID>MagicNumber:ChatGpt.kt$ChatGpt.Companion$429</ID>
<ID>MagicNumber:Comment.kt$Comment$3</ID>
<ID>MagicNumber:CryptoPrices.kt$CryptoPrices$10</ID>
<ID>MagicNumber:CurrencyConverter.kt$CurrencyConverter$11</ID>
@ -47,7 +45,6 @@
<ID>MagicNumber:WorldTime.kt$WorldTime.Companion$86.4</ID>
<ID>NestedBlockDepth:Addons.kt$Addons$fun add(command: AbstractCommand): Boolean</ID>
<ID>NestedBlockDepth:Addons.kt$Addons$fun add(module: AbstractModule): Boolean</ID>
<ID>NestedBlockDepth:ChatGpt.kt$ChatGpt.Companion$@JvmStatic @Throws(ModuleException::class) fun chat(query: String, apiKey: String?, maxTokens: Int): String</ID>
<ID>NestedBlockDepth:Comment.kt$Comment$override fun commandResponse(channel: String, args: String, event: GenericMessageEvent)</ID>
<ID>NestedBlockDepth:CurrencyConverter.kt$CurrencyConverter.Companion$@JvmStatic @Throws(ModuleException::class) fun loadSymbols(apiKey: String?)</ID>
<ID>NestedBlockDepth:CurrencyConverter.kt$CurrencyConverter.Companion$@JvmStatic fun convertCurrency(apiKey: String?, query: String): Message</ID>
@ -71,7 +68,6 @@
<ID>ReturnCount:Addons.kt$Addons$fun help(channel: String, topic: String, event: GenericMessageEvent): Boolean</ID>
<ID>ReturnCount:ExceptionSanitizer.kt$ExceptionSanitizer$fun ModuleException.sanitize(vararg sanitize: String): ModuleException</ID>
<ID>ReturnCount:Seen.kt$Seen$override fun commandResponse(channel: String, args: String, event: GenericMessageEvent)</ID>
<ID>ThrowsCount:ChatGpt.kt$ChatGpt.Companion$@JvmStatic @Throws(ModuleException::class) fun chat(query: String, apiKey: String?, maxTokens: Int): String</ID>
<ID>ThrowsCount:GoogleSearch.kt$GoogleSearch.Companion$@JvmStatic @Throws(ModuleException::class) fun searchGoogle( query: String, apiKey: String?, cseKey: String?, quotaUser: String = ReleaseInfo.PROJECT ): List&lt;Message&gt;</ID>
<ID>ThrowsCount:Joke.kt$Joke.Companion$@JvmStatic @Throws(ModuleException::class) fun randomJoke(): List&lt;Message&gt;</ID>
<ID>ThrowsCount:Mastodon.kt$Mastodon.Companion$@JvmStatic @Throws(ModuleException::class) fun toot(apiKey: String?, instance: String?, handle: String?, message: String, isDm: Boolean): String</ID>
@ -80,7 +76,6 @@
<ID>ThrowsCount:Weather2.kt$Weather2.Companion$@JvmStatic @Throws(ModuleException::class) fun getWeather(query: String, apiKey: String?): List&lt;Message&gt;</ID>
<ID>ThrowsCount:WolframAlpha.kt$WolframAlpha.Companion$@JvmStatic @Throws(ModuleException::class) fun queryWolfram(query: String, units: String = IMPERIAL, appId: String?): String</ID>
<ID>TooGenericExceptionCaught:ChatGpt2.kt$ChatGpt2.Companion$e: Exception</ID>
<ID>TooGenericExceptionCaught:Gemini.kt$Gemini.Companion$e: Exception</ID>
<ID>TooGenericExceptionCaught:Gemini2.kt$Gemini2.Companion$e: Exception</ID>
<ID>TooGenericExceptionCaught:StockQuote.kt$StockQuote.Companion$e: NullPointerException</ID>
<ID>TooGenericExceptionCaught:Weather2.kt$Weather2.Companion$e: NullPointerException</ID>
@ -94,9 +89,9 @@
<ID>WildcardImport:FeedReaderTest.kt$import assertk.assertions.*</ID>
<ID>WildcardImport:FeedsManager.kt$import com.rometools.rome.feed.synd.*</ID>
<ID>WildcardImport:Gemini2Test.kt$import assertk.assertions.*</ID>
<ID>WildcardImport:GeminiTest.kt$import assertk.assertions.*</ID>
<ID>WildcardImport:GoogleSearchTest.kt$import assertk.assertions.*</ID>
<ID>WildcardImport:JokeTest.kt$import assertk.assertions.*</ID>
<ID>WildcardImport:LinksManagerTest.kt$import assertk.assertions.*</ID>
<ID>WildcardImport:Mobibot.kt$import java.io.*</ID>
<ID>WildcardImport:Mobibot.kt$import net.thauvin.erik.mobibot.commands.*</ID>
<ID>WildcardImport:Mobibot.kt$import net.thauvin.erik.mobibot.commands.links.*</ID>

View file

@ -4,7 +4,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>net.thauvin.erik.mobibot</groupId>
<artifactId>mobibot</artifactId>
<version>0.8.0-rc+20250424113056</version>
<version>0.8.0-rc+20250506091211</version>
<name>mobibot</name>
<description></description>
<url></url>
@ -168,7 +168,7 @@
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.19.1</version>
<version>1.20.1</version>
<scope>compile</scope>
</dependency>
<dependency>

View file

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

View file

@ -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;
repositories = List.of(
MAVEN_LOCAL,
MAVEN_CENTRAL,
@ -166,10 +167,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().progressive(true).verbose(true);
op.execute();
}
@Override

View file

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

View file

@ -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.
*/
@ -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.
*/

View file

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

View file

@ -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} <command>", 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<PircBotX>()) {
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<PircBotX>()) {
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: <command>
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<PircBotX>()) {
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)
@ -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} <command>", 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<PircBotX>()) {
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<PircBotX>()) {
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: <command>
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<PircBotX>()) {
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)
}
}
}

View file

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

View file

@ -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+20250507140607"
@JvmField
@Suppress("MagicNumber")
val BUILD_DATE: LocalDateTime = LocalDateTime.ofInstant(
Instant.ofEpochMilli(1742629261438L), ZoneId.systemDefault()
Instant.ofEpochMilli(1746651967388L), ZoneId.systemDefault()
)
const val WEBSITE = "https://mobitopia.org/mobibot/"

View file

@ -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<String>.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.
*/
@ -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.
*/

View file

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

View file

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

View file

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

View file

@ -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<String>()
@ -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)
}
}

View file

@ -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<String>()
@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<String>()
@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)) {

View file

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

View file

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

View file

@ -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<String>, private val disabledModules: List<String>) : AbstractCommand() {
override val name = "modules"
override val help = listOf("To view a list of enabled/disabled modules:", helpFormat("%c $name"))

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<String> = mutableListOf()
private val keywords: MutableList<String> = 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)
}
@ -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")
@ -187,6 +199,10 @@ class LinksManager : AbstractCommand() {
}
}
/**
* Matches [keywords] in the given title and adds them to the provided tag list.
*/
internal fun matchTagKeywords(title: String, tags: MutableList<String>) {
for (match in keywords) {
val m = Regex.escape(match)

View file

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

View file

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

View file

@ -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 [<start>] [<query>]`
*/
internal fun parseArgs(args: String): Pair<Int, String> {
var query = args.lowercase().trim()
var start = 0
@ -88,7 +96,7 @@ class View : AbstractCommand() {
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

View file

@ -33,13 +33,15 @@ 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<String>, Serializable {
override fun compare(a: String, b: String): Int {
return a.lowercase().compareTo(b.lowercase())
}
companion object {
@Suppress("ConstPropertyName")
private const val serialVersionUID = 1L
}
}

View file

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

View file

@ -33,9 +33,11 @@ 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")
private const val serialVersionUID = 1L
}
}

View file

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

View file

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

View file

@ -36,7 +36,7 @@ import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
/**
* Tell Message.
* Holds a [Tell] message.
*/
class TellMessage(
/**
@ -98,7 +98,6 @@ class TellMessage(
}
companion object {
@Suppress("ConstPropertyName")
private const val serialVersionUID = 2L
}
}

View file

@ -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 = "",

View file

@ -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 {
/**

View file

@ -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 {
/**
@ -46,7 +46,6 @@ data class EntryComment(var comment: String, var nick: String) : Serializable {
companion object {
// Serial version UID
@Suppress("ConstPropertyName")
private const val serialVersionUID: Long = 1L
}
}

View file

@ -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 {
// Serial version UID
private const val serialVersionUID: Long = 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
}
}

View file

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

View file

@ -40,29 +40,13 @@ 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"
@ -84,4 +68,20 @@ class Calc : AbstractModule() {
help.add("To solve a mathematical calculation:")
help.add(helpFormat("%c $CALC_CMD <calculation>"))
}
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)
}
}
}

View file

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

View file

@ -43,7 +43,7 @@ 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)
@ -102,7 +102,7 @@ class CryptoPrices : AbstractModule() {
private const val CODES_KEYWORD = "codes"
/**
* Get current market price.
* Get the current market price.
*/
@JvmStatic
fun currentPrice(args: List<String>): CryptoPrice {

View file

@ -50,78 +50,13 @@ 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.
@ -219,4 +154,69 @@ 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()) -> {
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
}
}

View file

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

View file

@ -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,28 @@ 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)
}
}
} else {
helpResponse(event)
}
}
}

View file

@ -31,7 +31,7 @@
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.capitalize
import net.thauvin.erik.mobibot.Utils.colorize
import net.thauvin.erik.mobibot.Utils.encodeUrl
import net.thauvin.erik.mobibot.Utils.helpFormat
@ -51,43 +51,13 @@ 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)
}
}
companion object {
// Google API Key property
const val API_KEY_PROP = "google-api-key"
@ -112,7 +82,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."
"${GOOGLE_CMD.capitalize()} is disabled. The API keys are missing."
)
}
val results = mutableListOf<Message>()
@ -159,4 +129,34 @@ class GoogleSearch : AbstractModule() {
help.add(helpFormat("%c $GOOGLE_CMD <query>"))
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)
}
}
}

View file

@ -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,22 @@ 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) {
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)
}
}
}
}
}

View file

@ -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 <ip address or hostname>"))
}
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)
}
}
}

View file

@ -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,32 +59,6 @@ class Mastodon : SocialModule() {
override val isValidProperties: Boolean
get() = !(properties[INSTANCE_PROP].isNullOrBlank() || properties[ACCESS_TOKEN_PROP].isNullOrBlank())
/**
* Formats the entry for posting.
*/
override fun formatEntry(entry: EntryLink): String {
return "${entry.title} (via ${entry.nick} on ${entry.channel})${formatTags(entry)}\n\n${entry.link}"
}
private fun formatTags(entry: EntryLink): String {
return entry.tags.filter { !it.name.equals(entry.channel.removePrefix("#"), true) }
.joinToString(separator = " ", prefix = "\n\n") { "#${it.name}" }
}
/**
* Posts on Mastodon.
*/
@Throws(ModuleException::class)
override fun post(message: String, isDm: Boolean): String {
return toot(
apiKey = 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"
@ -146,4 +123,30 @@ class Mastodon : SocialModule() {
properties[AUTO_POST_PROP] = "false"
initProperties(ACCESS_TOKEN_PROP, HANDLE_PROP, INSTANCE_PROP)
}
/**
* Formats the entry for posting.
*/
override fun formatEntry(entry: EntryLink): String {
return "${entry.title} (via ${entry.nick} on ${entry.channel})${formatTags(entry)}\n\n${entry.link}"
}
private fun formatTags(entry: EntryLink): String {
return entry.tags.filter { !it.name.equals(entry.channel.removePrefix("#"), true) }
.joinToString(separator = " ", prefix = "\n\n") { "#${it.name}" }
}
/**
* Posts on Mastodon.
*/
@Throws(ModuleException::class)
override fun post(message: String, isDm: Boolean): String {
return toot(
apiKey = properties[ACCESS_TOKEN_PROP],
instance = properties[INSTANCE_PROP],
handle = handle,
message = message,
isDm = isDm
)
}
}

View file

@ -30,16 +30,12 @@
*/
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")
private const val serialVersionUID = 1L
}
}

View file

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

View file

@ -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,20 +94,6 @@ 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()]

View file

@ -30,7 +30,7 @@
*/
package net.thauvin.erik.mobibot.modules
import net.thauvin.erik.mobibot.Utils.capitalise
import net.thauvin.erik.mobibot.Utils.capitalize
import net.thauvin.erik.mobibot.Utils.encodeUrl
import net.thauvin.erik.mobibot.Utils.helpFormat
import net.thauvin.erik.mobibot.Utils.reader
@ -49,34 +49,13 @@ 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)
}
}
companion object {
/**
* The API property key.
@ -133,7 +112,7 @@ class StockQuote : AbstractModule() {
if (apiKey.isNullOrBlank()) {
throw ModuleException(
"${StockQuote::class.java.name} is disabled.",
"${STOCK_CMD.capitalise()} is disabled. The API key is missing."
"${STOCK_CMD.capitalize()} is disabled. The API key is missing."
)
}
val messages = mutableListOf<Message>()
@ -233,4 +212,25 @@ class StockQuote : AbstractModule() {
help.add(helpFormat("%c $STOCK_CMD <symbol|keywords>"))
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)
}
}
}

View file

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

View file

@ -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,38 +51,13 @@ 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)
}
}
companion object {
/**
* The OpenWeatherMap API Key property.
@ -121,7 +96,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 +156,7 @@ class Weather2 : AbstractModule() {
for (w in it) {
w?.let {
condition.append(' ')
.append(w.getDescription().capitalise())
.append(w.getDescription().capitalize())
.append('.')
}
}
@ -247,4 +222,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)
}
}
}

View file

@ -47,40 +47,6 @@ class WolframAlpha : AbstractModule() {
override val name = "WolframAlpha"
private fun getUnits(unit: String?): String {
return if (unit?.lowercase() == METRIC) {
METRIC
} else {
IMPERIAL
}
}
override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
if (args.isNotBlank()) {
try {
val query = args.trim().split("units=", limit = 2, ignoreCase = true)
event.sendMessage(
queryWolfram(
query[0].trim(),
units = if (query.size == 2) {
getUnits(query[1].trim())
} else {
getUnits(properties[UNITS_PROP])
},
appId = properties[APPID_KEY_PROP]
)
)
} catch (e: ModuleException) {
if (logger.isWarnEnabled) logger.warn(e.debugMessage, e)
e.message?.let {
event.respond(it)
}
}
} else {
helpResponse(event)
}
}
companion object {
/**
* The Wolfram Alpha API Key property.
@ -139,4 +105,41 @@ class WolframAlpha : AbstractModule() {
}
initProperties(APPID_KEY_PROP, UNITS_PROP)
}
private fun getUnits(unit: String?): String {
return if (unit?.lowercase() == METRIC) {
METRIC
} else {
IMPERIAL
}
}
/**
* Queries Wolfram Alpha.
*/
override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
if (args.isNotBlank()) {
try {
val query = args.trim().split("units=", limit = 2, ignoreCase = true)
event.sendMessage(
queryWolfram(
query[0].trim(),
units = if (query.size == 2) {
getUnits(query[1].trim())
} else {
getUnits(properties[UNITS_PROP])
},
appId = properties[APPID_KEY_PROP]
)
)
} catch (e: ModuleException) {
if (logger.isWarnEnabled) logger.warn(e.debugMessage, e)
e.message?.let {
event.respond(it)
}
}
} else {
helpResponse(event)
}
}
}

View file

@ -367,6 +367,16 @@ class WorldTime : AbstractModule() {
}
}
init {
with(help) {
add("To display a country's current date/time:")
add(helpFormat("%c $TIME_CMD [<country code or zone>]"))
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 [<country code or zone>]"))
add("For a listing of the supported countries/zones:")
add(helpFormat("%c $TIME_CMD $ZONES_ARGS"))
}
commands.add(TIME_CMD)
}
}

View file

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

View file

@ -30,8 +30,9 @@
*/
package net.thauvin.erik.mobibot.msg
/**
* The `Message` class.
* Holds a message.
*/
open class Message @JvmOverloads constructor(
var msg: String,

View file

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

View file

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

View file

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

View file

@ -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<Int> = HashSet()

View file

@ -40,6 +40,10 @@ import org.pircbotx.hooks.types.GenericMessageEvent
import org.slf4j.Logger
import org.slf4j.LoggerFactory
/**
* Provides the ability to handle notifications, post entries and manage interaction with a specific social media
* service.
*/
abstract class SocialModule : AbstractModule() {
private val logger: Logger = LoggerFactory.getLogger(SocialManager::class.java)

View file

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

View file

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

View file

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

View file

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

View file

@ -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 {
@Nested
@DisplayName("Failure Tests")
inner class FailureTests {
@Test
fun readFeedTest() {
var messages = readFeed("https://feeds.thauvin.net/ethauvin")
fun invalidFeed() {
assertFailure { readFeed("https://www.example.com") }.isInstanceOf(FeedException::class.java)
}
@Test
fun invalidHost() {
assertFailure { readFeed("https://www.examplesfoo.com/") }.isInstanceOf(UnknownHostException::class.java)
}
@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")
}
}
messages = readFeed("https://lorem-rss.herokuapp.com/feed?length=0")
assertThat(messages, "messages").index(0).prop(Message::msg).contains("nothing")
messages = readFeed("https://lorem-rss.herokuapp.com/feed?length=84", 42)
@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/")
}
}
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)
}
}
}

View file

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

View file

@ -40,30 +40,17 @@ 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 {
@ -78,4 +65,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")
}
}

View file

@ -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,137 +73,221 @@ 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."
@Nested
@DisplayName("Date Tests")
inner class DateTests {
private val cal = Calendar.getInstance()
private val localDateTime = LocalDateTime.of(1952, 2, 17, 12, 30, 0)
@BeforeEach
fun setUp() {
fun beforeEach() {
cal[1952, Calendar.FEBRUARY, 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")
fun `Convert a Date to an ISO date`() {
assertThat(cal.time.toIsoLocalDate(), "isoLocalDate(date)").isEqualTo("1952-02-17")
}
@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("")
fun `Convert a LocalDate to an ISO date`() {
assertThat(localDateTime.toIsoLocalDate(), "isoLocalDate(localDate)").isEqualTo("1952-02-17")
}
@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 ")
fun `Convert a Date to a UTC date-time`() {
assertThat(cal.time.toUtcDateTime(), "utcDateTime(date)").isEqualTo("1952-02-17 12:30")
}
@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)
fun `Convert a LocalDate to a UTC date-time`() {
assertThat(localDateTime.toUtcDateTime(), "utcDateTime(localDate)").isEqualTo("1952-02-17 12:30")
}
@Test
fun testCyan() {
assertThat(ascii.cyan()).isEqualTo(Colors.CYAN + ascii + Colors.NORMAL)
fun `Today should return the current date in ISO format`() {
assertThat(today()).isEqualTo(LocalDateTime.now().toIsoLocalDate())
}
}
@Test
fun testEncodeUrl() {
assertThat("Hello Günter".encodeUrl()).isEqualTo("Hello%20G%C3%BCnter")
}
@Nested
@DisplayName("Help Tests")
inner class HelpTests {
private val bot = "mobibot"
@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")
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")
}
@Test
fun testHelpFormat() {
fun `Construct help string for private message`() {
assertThat(helpCmdSyntax("%c $test %n $test", bot, false), "helpCmdSyntax(private)")
.isEqualTo("$bot: $test $bot $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 testIsoLocalDate() {
assertThat(cal.time.toIsoLocalDate(), "isoLocalDate(date)").isEqualTo("1952-02-17")
assertThat(localDateTime.toIsoLocalDate(), "isoLocalDate(localDate)").isEqualTo("1952-02-17")
}
@Nested
@DisplayName("Properties Tests")
inner class PropertiesTests {
@Test
fun testLastOrEmpty() {
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)
}
}
@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("")
}
}
@Nested
@DisplayName("String Manipulation Tests")
inner class StringManipulationTests {
private val dir = "dir"
private val sep = '/'
private val url = "https://erik.thauvin.net"
@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 testObfuscate() {
fun `Append separator char if already present`() {
assertThat(url.appendIfMissing(sep), "appendIfMissing(url)").isEqualTo("$url$sep")
}
@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)))
}
assertThat(" ".obfuscate(), "obfuscate(blank)").isEqualTo(" ")
}
@Test
fun testPlural() {
fun `Obfuscate empty string`() {
assertThat(" ".obfuscate(), "obfuscate(blank)").isEqualTo(" ")
}
}
@Test
fun `Pluralize string`() {
val week = "week"
val weeks = "weeks"
@ -210,66 +296,133 @@ class UtilsTest {
}
}
@Nested
@DisplayName("Replace Tests")
inner class ReplaceTests {
private val replace = arrayOf("1", "2", "3")
private val search = arrayOf("one", "two", "three")
@Test
fun testReplaceEach() {
val search = arrayOf("one", "two", "three")
val replace = arrayOf("1", "2", "3")
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 testRed() {
assertThat(ascii.red()).isEqualTo(ascii.colorize(Colors.RED))
}
@Test
fun testReverseColor() {
assertThat(ascii.reverseColor()).isEqualTo(Colors.REVERSE + ascii + Colors.REVERSE)
}
@Test
fun testToday() {
assertThat(today()).isEqualTo(LocalDateTime.now().toIsoLocalDate())
}
@Test
fun testToIntOrDefault() {
assertThat("10".toIntOrDefault(1), "toIntOrDefault(10, 1)").isEqualTo(10)
assertThat("a".toIntOrDefault(2), "toIntOrDefault(a, 2)").isEqualTo(2)
}
@Test
fun testUnderline() {
assertThat(ascii.underline()).isEqualTo(ascii.colorize(Colors.UNDERLINE))
}
@Test
fun testUnescapeXml() {
fun `Unescape XML`() {
assertThat("&lt;a name=&quot;test &amp; &apos;&#39;&quot;&gt;".unescapeXml()).isEqualTo(
"<a name=\"test & ''\">"
)
}
}
@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)
}
@Nested
@DisplayName("Text Styling Tests")
inner class TextStylingTests {
@Nested
@DisplayName("Colorize Tests")
inner class ColorizeTests {
@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")
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))
}
}
}

View file

@ -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 {
@Nested
@DisplayName("Uptime Tests")
inner class UptimeTests {
@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")
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")
}
}
}

View file

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

View file

@ -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()
@Nested
@DisplayName("Fetch Page Title Tests")
inner class FetchPageTitleTests {
@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)
fun fetchPageTitle() {
assertThat(linksManager.fetchPageTitle("https://erik.thauvin.net/")).contains("Erik's Weblog")
}
@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()
fun fetchPageNoTitle() {
assertThat(linksManager.fetchPageTitle("https://www.google.com/foo")).isEqualTo(Constants.NO_TITLE)
}
}
@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() {
fun matchInvalidProtocol() {
assertThat(linksManager.matches("ftp://erik.thauvin.net/blog/")).isFalse()
}
@Test
fun matchLink() {
assertThat(linksManager.matches("https://erik.thauvin.net/blog/")).isTrue()
}
@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<String>()
linksManager.matchTagKeywords("Test title with key2", tags)
assertThat(tags, "tags").contains("key2")
tags.clear()
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 {
contains("key1")
contains("key3")
containsExactlyInAnyOrder("key1", "key3")
size().isEqualTo(2)
}
}
}
}
}

View file

@ -36,13 +36,13 @@ 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()
companion object {
init {
for (i in 1..10) {
LinksManager.entries.links.add(
EntryLink(
@ -55,57 +55,84 @@ class ViewTest {
)
)
}
assertThat(view.parseArgs("1"), "parseArgs(1)").all {
prop(Pair<Int, String>::first).isEqualTo(0)
prop(Pair<Int, String>::second).isEqualTo("")
}
}
assertThat(view.parseArgs("2 foo"), "parseArgs(2, foo)").all {
prop(Pair<Int, String>::first).isEqualTo(1)
prop(Pair<Int, String>::second).isEqualTo("foo")
}
assertThat(view.parseArgs("3 FOO"), "parseArgs(3, FOO)").all {
prop(Pair<Int, String>::first).isEqualTo(2)
prop(Pair<Int, String>::second).isEqualTo("foo")
}
assertThat(view.parseArgs(" 4 foo bar "), "parseArgs( 4 foo bar )").all {
prop(Pair<Int, String>::first).isEqualTo(3)
prop(Pair<Int, String>::second).isEqualTo("foo bar")
}
assertThat(view.parseArgs("foo bar"), "parseArgs(foo bar)").all {
prop(Pair<Int, String>::first).isEqualTo(0)
prop(Pair<Int, String>::second).isEqualTo("foo bar")
}
assertThat(view.parseArgs("${Int.MAX_VALUE}1"), "parseArgs(overflow)").all {
prop(Pair<Int, String>::first).isEqualTo(0)
prop(Pair<Int, String>::second).isEqualTo("${Int.MAX_VALUE}1")
}
@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<Int, String>::first).isEqualTo(0)
prop(Pair<Int, String>::second).isEqualTo("1a")
}
assertThat(view.parseArgs("20"), "parseArgs(20)").all {
prop(Pair<Int, String>::first).isEqualTo(0)
prop(Pair<Int, String>::second).isEqualTo("")
}
@Test
fun `Parse empty arguments`() {
assertThat(view.parseArgs(""), "parseArgs()").all {
prop(Pair<Int, String>::first).isEqualTo(LinksManager.entries.links.size - View.MAX_ENTRIES)
prop(Pair<Int, String>::second).isEqualTo("")
}
}
LinksManager.entries.links.clear()
assertThat(view.parseArgs("4"), "parseArgs(4)").all {
@Test
fun `Parse first item`() {
assertThat(view.parseArgs("1"), "parseArgs(1)").all {
prop(Pair<Int, String>::first).isEqualTo(0)
prop(Pair<Int, String>::second).isEqualTo("")
}
}
@Test
fun `Parse fourth item with query needing trimming`() {
assertThat(view.parseArgs(" 4 foo bar "), "parseArgs( 4 foo bar )").all {
prop(Pair<Int, String>::first).isEqualTo(3)
prop(Pair<Int, String>::second).isEqualTo("foo bar")
}
}
@Test
fun `Parse overflowed item as query`() {
assertThat(view.parseArgs("${Int.MAX_VALUE}1"), "parseArgs(overflow)").all {
prop(Pair<Int, String>::first).isEqualTo(0)
prop(Pair<Int, String>::second).isEqualTo("${Int.MAX_VALUE}1")
}
}
@Test
fun `Parse out of bounds item`() {
assertThat(view.parseArgs("20"), "parseArgs(20)").all {
prop(Pair<Int, String>::first).isEqualTo(0)
prop(Pair<Int, String>::second).isEqualTo("")
}
}
@Test
fun `Parse query only`() {
assertThat(view.parseArgs("foo bar"), "parseArgs(foo bar)").all {
prop(Pair<Int, String>::first).isEqualTo(0)
prop(Pair<Int, String>::second).isEqualTo("foo bar")
}
}
@Test
fun `Parse second item with query`() {
assertThat(view.parseArgs("2 foo"), "parseArgs(2, foo)").all {
prop(Pair<Int, String>::first).isEqualTo(1)
prop(Pair<Int, String>::second).isEqualTo("foo")
}
}
@Test
fun `Parse third item with query ignoring capitalization`() {
assertThat(view.parseArgs("3 FOO"), "parseArgs(3, FOO)").all {
prop(Pair<Int, String>::first).isEqualTo(2)
prop(Pair<Int, String>::second).isEqualTo("foo")
}
}
}
}

View file

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

View file

@ -47,7 +47,7 @@ class TellMessageTest {
}
@Test
fun testTellMessage() {
fun validateTellMessage() {
val message = "Test message."
val recipient = "recipient"
val sender = "sender"

View file

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

View file

@ -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,13 +60,16 @@ class EntriesUtilsTest {
}
}
@Nested
@DisplayName("Print Tests")
inner class PrintTests {
@Test
fun printCommentTest() {
fun printComment() {
assertThat(printComment(0, 0, comment)).isEqualTo("${Constants.LINK_CMD}1.1: [nick] comment")
}
@Test
fun printLinkTest() {
fun printLink() {
for (i in links.indices) {
assertThat(
printLink(i - 1, links[i]), "link $i"
@ -76,16 +81,17 @@ class EntriesUtilsTest {
}
@Test
fun printTagsTest() {
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")
}
}

View file

@ -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()
}
@Nested
@DisplayName("Validate Tags Test")
inner class ValidateTagsTest {
@Test
fun testTags() {
fun `Validate tags parsing in constructor`() {
val tags: List<SyndCategory> = entryLink.tags
for ((i, tag) in tags.withIndex()) {
assertThat(tag.name, "tag.name($i)").isEqualTo("tag${i + 1}")
}
assertThat(entryLink::tags).size().isEqualTo(5)
entryLink.setTags("-tag5, tag4")
entryLink.setTags("+mobitopia")
entryLink.setTags("-mobitopia")
}
@Test
fun `Validate attempting to remove channel tag`() {
val link = entryLink
link.setTags("+mobitopia")
link.setTags("-mobitopia") // can't remove the channel tag
assertThat(
entryLink.formatTags(","),
"formatTags(',')"
).isEqualTo("tag1,tag2,tag3,tag4,mobitopia")
entryLink.setTags("-tag4 tag5")
link.formatTags(",")
).isEqualTo("tag1,tag2,tag3,tag4,tag5,mobitopia")
}
@Test
fun `Validate formatting tags with spaces`() {
val link = entryLink
link.setTags("-tag4")
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)
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)
}
}
}

View file

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

View file

@ -41,10 +41,33 @@ import kotlin.test.Test
class CalcTest {
@Test
fun testCalculate() {
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 `Invalid calculation show throw exception`() {
assertFailure { calculate("one + one") }.isInstanceOf(UnknownFunctionOrVariableException::class.java)
}
}

View file

@ -32,30 +32,45 @@ package net.thauvin.erik.mobibot.modules
import assertk.assertFailure
import assertk.assertThat
import assertk.assertions.contains
import assertk.assertions.hasNoCause
import assertk.assertions.isInstanceOf
import assertk.assertions.contains
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 kotlin.test.Test
class ChatGpt2Test : LocalProperties() {
@Test
fun testApiKey() {
fun apiKey() {
assertFailure { ChatGpt2.chat("1 gallon to liter", "", 0) }
.isInstanceOf(ModuleException::class.java)
.hasNoCause()
}
@Nested
@DisplayName("Chat Tests")
inner class ChatTests {
private val apiKey = getProperty(ChatGpt2.API_KEY_PROP)
@Test
@DisableOnCi
fun testChat() {
val apiKey = getProperty(ChatGpt2.API_KEY_PROP)
fun chat() {
assertThat(
ChatGpt2.chat("how do I make an HTTP request in Javascript?", apiKey, 200)
).contains("XMLHttpRequest")
ChatGpt2.chat(
"return only the code for javascript function to make a request with XMLHttpRequest",
apiKey,
50
)
).contains("```javascript")
}
@Test
@DisableOnCi
fun chatFailure() {
assertFailure { ChatGpt2.chat("1 liter to gallon", apiKey, -1) }
.isInstanceOf(ModuleException::class.java)
}
}
}

View file

@ -40,6 +40,8 @@ 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 java.util.logging.ConsoleHandler
import java.util.logging.Level
import kotlin.test.Test
@ -49,30 +51,6 @@ class CryptoPricesTest {
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 +62,44 @@ class CryptoPricesTest {
}
}
}
@Nested
@DisplayName("Current Price Tests")
inner class CurrentPriceTests {
@Test
@Throws(ModuleException::class)
fun currentPriceBitcoin() {
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 currentPriceEthereum() {
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)
}
}
}
@Nested
@DisplayName("Currency Name Tests")
inner class CurrencyNameTests {
@Test
fun getCurrencyNameUsd() {
assertThat(getCurrencyName("USD"), "USD").isEqualTo("United States Dollar")
}
@Test
fun getCurrencyNameEur() {
assertThat(getCurrencyName("EUR"), "EUR").isEqualTo("Euro")
}
}
}

View file

@ -42,6 +42,8 @@ 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 kotlin.test.Test
class CurrencyConverterTest : LocalProperties() {
@ -50,28 +52,49 @@ class CurrencyConverterTest : LocalProperties() {
loadSymbols(apiKey)
}
@Nested
@DisplayName("Currency Converter Tests")
inner class CurrencyConverterTests {
private val apiKey = getProperty(CurrencyConverter.API_KEY_PROP)
@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())
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)
}
}
}
}

View file

@ -35,19 +35,64 @@ package net.thauvin.erik.mobibot.modules
import assertk.assertThat
import assertk.assertions.isEqualTo
import assertk.assertions.matches
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.RepeatedTest
import kotlin.random.Random
import kotlin.test.Test
class DiceTest {
@Nested
@DisplayName("Roll Tests")
inner class RollTests {
@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)")
fun `Roll 1 die with 1 side`() {
assertThat(Dice.roll(1, 1)).isEqualTo("\u00021\u0002")
}
@Test
fun `Roll 1 die with 6 sides`() {
assertThat(Dice.roll(1, 6)).matches("\u0002[1-6]\u0002".toRegex())
}
@RepeatedTest(5)
fun `Roll 1 die with random sides`() {
assertThat(Dice.roll(1, Random.nextInt(1, 11))).matches("\u0002([1-9]|10)\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 2 dice with 6 sides`() {
assertThat(Dice.roll(2, 6))
.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())
}
@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()
)
}
}
}

View file

@ -35,31 +35,53 @@ 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 kotlin.test.Test
class Gemini2Test : LocalProperties() {
@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
fun testApiKey() {
assertFailure { Gemini2.chat("1 gallon to liter", "", 0) }
.isInstanceOf(ModuleException::class.java)
.hasNoCause()
@DisableOnCi
fun chatHttpRequestInJavascript() {
assertThat(
Gemini2.chat(
"return only the code for a javascript function to make a request with XMLHttpRequest",
apiKey,
maxTokens
)
).isNotNull().contains("```javascript")
}
@Test
@DisableOnCi
fun chatPrompt() {
val apiKey = getProperty(Gemini2.GEMINI_API_KEY)
val maxTokens = getProperty(Gemini2.MAX_TOKENS_PROP).toInt()
assertThat(
Gemini2.chat("how do I make an HTTP request in Javascript?", apiKey, maxTokens)
).isNotNull().contains("XMLHttpRequest")
fun chatEncodeUrlInJava() {
assertThat(
Gemini2.chat("how do I encode a URL in java?", apiKey, 60)
).isNotNull().contains("URLEncoder")
}
}
@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)
}
}
}

View file

@ -40,48 +40,15 @@ 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 kotlin.test.Test
class GoogleSearchTest : LocalProperties() {
@Test
fun testAPIKeys() {
assertThat(
searchGoogle("", "apikey", "cssKey").first(),
"searchGoogle(empty)"
).isInstanceOf(ErrorMessage::class.java)
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<Message> {
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 +58,63 @@ class GoogleSearchTest : LocalProperties() {
}
}
}
@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 {
private val apiKey = getProperty(GoogleSearch.API_KEY_PROP)
private val cseKey = getProperty(GoogleSearch.CSE_KEY_PROP)
@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)
}
}
}
}

View file

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

View file

@ -35,22 +35,32 @@ import assertk.assertions.any
import assertk.assertions.contains
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 kotlin.test.Test
class LookupTest {
@Nested
@DisplayName("Lookup Tests")
inner class LookupTests {
@Test
@Throws(Exception::class)
fun testLookup() {
var result = nslookup("apple.com")
fun lookupByHostname() {
val result = nslookup("apple.com")
assertThat(result, "lookup(apple.com)").contains("17.253.144.10")
result = nslookup("37.27.52.13")
assertThat(result, "lookup(37.27.52.13)").contains("nix4.thauvin.us")
}
@Test
@Throws(Exception::class)
fun testWhois() {
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 whois() {
val result = whois("17.178.96.59", Lookup.WHOIS_HOST)
assertThat(result, "whois(17.178.96.59").any { it.contains("Apple Inc.") }
}

View file

@ -39,7 +39,7 @@ import kotlin.test.Test
class MastodonTest : LocalProperties() {
@Test
@Throws(ModuleException::class)
fun testToot() {
fun `Toot on Mastodon`() {
val msg = "Testing Mastodon API from ${getHostName()}"
assertThat(
toot(

View file

@ -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,29 +47,32 @@ class ModuleExceptionTest {
const val MESSAGE = "message"
@JvmStatic
fun dataProviders(): List<ModuleException> {
fun moduleExceptions(): List<ModuleException> {
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)
)
}
}
@Nested
@DisplayName("Message Tests")
inner class MessageTests {
@ParameterizedTest
@MethodSource("dataProviders")
fun testGetDebugMessage(e: ModuleException) {
@MethodSource("net.thauvin.erik.mobibot.modules.ModuleExceptionTest#moduleExceptions")
fun getDebugMessage(e: ModuleException) {
assertThat(e::debugMessage).isEqualTo(DEBUG_MESSAGE)
}
@ParameterizedTest
@MethodSource("dataProviders")
fun testGetMessage(e: ModuleException) {
@MethodSource("net.thauvin.erik.mobibot.modules.ModuleExceptionTest#moduleExceptions")
fun getMessage(e: ModuleException) {
assertThat(e).hasMessage(MESSAGE)
}
@Test
fun testSanitizeMessage() {
fun sanitizeMessage() {
val apiKey = "1234567890"
var e = ModuleException(DEBUG_MESSAGE, MESSAGE, IOException("URL http://foo.com?apiKey=$apiKey&userID=me"))
assertThat(
@ -102,3 +107,4 @@ class ModuleExceptionTest {
assertThat(e.sanitize(), "exception should be unchanged").isEqualTo(e)
}
}
}

View file

@ -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 radon 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()
}
}

View file

@ -34,17 +34,57 @@ package net.thauvin.erik.mobibot.modules
import assertk.assertThat
import assertk.assertions.isEqualTo
import net.thauvin.erik.mobibot.modules.RockPaperScissors.Companion.winLoseOrDraw
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Nested
import kotlin.test.Test
class RockPaperScissorsTest {
@Nested
@DisplayName("Win, Lose or Draw Tests")
inner class WinLoseOrDrawTests {
@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")
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")
}
}
}

Some files were not shown because too many files have changed in this diff Show more