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" /> <option name="jvmTarget" value="17" />
</component> </component>
<component name="KotlinCommonCompilerArguments"> <component name="KotlinCommonCompilerArguments">
<option name="apiVersion" value="2.0" /> <option name="apiVersion" value="2.2" />
<option name="languageVersion" value="2.0" /> <option name="languageVersion" value="2.2" />
</component> </component>
<component name="KotlinJpsPluginSettings"> <component name="KotlinJpsPluginSettings">
<option name="version" value="2.1.0" /> <option name="version" value="2.1.20" />
</component> </component>
</project> </project>

110
.idea/misc.xml generated
View file

@ -1,12 +1,122 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="EntryPointsManager"> <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="deploy" />
<pattern value="net.thauvin.erik.MobibotBuild" method="jacoco" /> <pattern value="net.thauvin.erik.MobibotBuild" method="jacoco" />
<pattern value="net.thauvin.erik.MobibotBuild" /> <pattern value="net.thauvin.erik.MobibotBuild" />
<pattern value="net.thauvin.erik.MobibotBuild" method="detekt" /> <pattern value="net.thauvin.erik.MobibotBuild" method="detekt" />
<pattern value="net.thauvin.erik.MobibotBuild" method="detektBaseline" /> <pattern value="net.thauvin.erik.MobibotBuild" method="detektBaseline" />
<pattern value="net.thauvin.erik.MobibotBuild" method="rootPom" /> <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>
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" project-jdk-name="17" project-jdk-type="JavaSDK"> <component name="ProjectRootManager" version="2" languageLevel="JDK_17" project-jdk-name="17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build" /> <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" ?> <?xml version="1.0" ?>
<SmellBaseline> <SmellBaseline>
<ManuallySuppressedIssues></ManuallySuppressedIssues> <ManuallySuppressedIssues/>
<CurrentIssues> <CurrentIssues>
<ID>CyclomaticComplexMethod:FeedsManager.kt$FeedsManager.Companion$@JvmStatic fun saveFeed(entries: Entries, currentFile: String = CURRENT_XML)</ID> <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> <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>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: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>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:Comment.kt$Comment$3</ID>
<ID>MagicNumber:CryptoPrices.kt$CryptoPrices$10</ID> <ID>MagicNumber:CryptoPrices.kt$CryptoPrices$10</ID>
<ID>MagicNumber:CurrencyConverter.kt$CurrencyConverter$11</ID> <ID>MagicNumber:CurrencyConverter.kt$CurrencyConverter$11</ID>
@ -47,7 +45,6 @@
<ID>MagicNumber:WorldTime.kt$WorldTime.Companion$86.4</ID> <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(command: AbstractCommand): Boolean</ID>
<ID>NestedBlockDepth:Addons.kt$Addons$fun add(module: AbstractModule): 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: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 @Throws(ModuleException::class) fun loadSymbols(apiKey: String?)</ID>
<ID>NestedBlockDepth:CurrencyConverter.kt$CurrencyConverter.Companion$@JvmStatic fun convertCurrency(apiKey: String?, query: String): Message</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: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: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>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: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: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> <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: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>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: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:Gemini2.kt$Gemini2.Companion$e: Exception</ID>
<ID>TooGenericExceptionCaught:StockQuote.kt$StockQuote.Companion$e: NullPointerException</ID> <ID>TooGenericExceptionCaught:StockQuote.kt$StockQuote.Companion$e: NullPointerException</ID>
<ID>TooGenericExceptionCaught:Weather2.kt$Weather2.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:FeedReaderTest.kt$import assertk.assertions.*</ID>
<ID>WildcardImport:FeedsManager.kt$import com.rometools.rome.feed.synd.*</ID> <ID>WildcardImport:FeedsManager.kt$import com.rometools.rome.feed.synd.*</ID>
<ID>WildcardImport:Gemini2Test.kt$import assertk.assertions.*</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:GoogleSearchTest.kt$import assertk.assertions.*</ID>
<ID>WildcardImport:JokeTest.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 java.io.*</ID>
<ID>WildcardImport:Mobibot.kt$import net.thauvin.erik.mobibot.commands.*</ID> <ID>WildcardImport:Mobibot.kt$import net.thauvin.erik.mobibot.commands.*</ID>
<ID>WildcardImport:Mobibot.kt$import net.thauvin.erik.mobibot.commands.links.*</ID> <ID>WildcardImport:Mobibot.kt$import net.thauvin.erik.mobibot.commands.links.*</ID>

View file

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

View file

@ -16,6 +16,8 @@ ident=changeme
logs=./logs logs=./logs
ignore=chanserv,nickserv ignore=chanserv,nickserv
tags=mobile mobitopia tags=mobile mobitopia
# Keywords to add as tags if found in links titles
tags-keywords=android ios apple google tags-keywords=android ios apple google
feed=http://www.mobitopia.org/rss.xml feed=http://www.mobitopia.org/rss.xml
@ -68,7 +70,7 @@ disabled-modules=mastodon
#alphavantage-api-key= #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-appid=
#wolfram-units=imperial #wolfram-units=imperial

View file

@ -38,7 +38,6 @@ import rife.bld.extension.CompileKotlinOperation;
import rife.bld.extension.DetektOperation; import rife.bld.extension.DetektOperation;
import rife.bld.extension.GeneratedVersionOperation; import rife.bld.extension.GeneratedVersionOperation;
import rife.bld.extension.JacocoReportOperation; import rife.bld.extension.JacocoReportOperation;
import rife.bld.extension.kotlin.CompileOptions;
import rife.bld.operations.exceptions.ExitStatusException; import rife.bld.operations.exceptions.ExitStatusException;
import rife.bld.publish.PomBuilder; import rife.bld.publish.PomBuilder;
import rife.tools.FileUtils; import rife.tools.FileUtils;
@ -72,8 +71,10 @@ public class MobibotBuild extends Project {
mainClass = pkg + ".Mobibot"; mainClass = pkg + ".Mobibot";
javaRelease = 17; javaRelease = 17;
downloadSources = true; downloadSources = true;
autoDownloadPurge = true; autoDownloadPurge = true;
repositories = List.of( repositories = List.of(
MAVEN_LOCAL, MAVEN_LOCAL,
MAVEN_CENTRAL, MAVEN_CENTRAL,
@ -166,10 +167,9 @@ public class MobibotBuild extends Project {
@Override @Override
public void compile() throws Exception { public void compile() throws Exception {
releaseInfo(); releaseInfo();
new CompileKotlinOperation() var op = new CompileKotlinOperation().fromProject(this);
.compileOptions(new CompileOptions().progressive(true).verbose(true)) op.compileOptions().progressive(true).verbose(true);
.fromProject(this) op.execute();
.execute();
} }
@Override @Override

View file

@ -41,7 +41,7 @@ import org.slf4j.LoggerFactory
import java.util.* import java.util.*
/** /**
* Modules and Commands addons. * Registers and manages commands and modules.
*/ */
class Addons(private val props: Properties) { class Addons(private val props: Properties) {
private val logger: Logger = LoggerFactory.getLogger(Addons::class.java) private val logger: Logger = LoggerFactory.getLogger(Addons::class.java)

View file

@ -30,10 +30,16 @@
*/ */
package net.thauvin.erik.mobibot package net.thauvin.erik.mobibot
/** /**
* The `Constants`. * The global constants.
*/ */
object Constants { object Constants {
/**
* CLI command for usage.
*/
const val CLI_CMD = "java -jar ${ReleaseInfo.PROJECT}.jar"
/** /**
* The connect/read timeout in ms. * The connect/read timeout in ms.
*/ */
@ -54,16 +60,6 @@ object Constants {
*/ */
const val DEFAULT_SERVER = "irc.libera.chat" 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. * The help command.
*/ */
@ -94,6 +90,11 @@ object Constants {
*/ */
const val TIMER_DELAY = 10L 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. * Properties version line argument.
*/ */

View file

@ -51,23 +51,6 @@ import java.net.URL
class FeedReader(private val url: String, val event: GenericMessageEvent) : Runnable { class FeedReader(private val url: String, val event: GenericMessageEvent) : Runnable {
private val logger: Logger = LoggerFactory.getLogger(FeedsManager::class.java) 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 { companion object {
@JvmStatic @JvmStatic
@Throws(FeedException::class, IOException::class) @Throws(FeedException::class, IOException::class)
@ -89,4 +72,21 @@ class FeedReader(private val url: String, val event: GenericMessageEvent) : Runn
return messages 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 kotlinx.cli.default
import net.thauvin.erik.mobibot.Utils.appendIfMissing import net.thauvin.erik.mobibot.Utils.appendIfMissing
import net.thauvin.erik.mobibot.Utils.bot 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.getIntProperty
import net.thauvin.erik.mobibot.Utils.helpCmdSyntax import net.thauvin.erik.mobibot.Utils.helpCmdSyntax
import net.thauvin.erik.mobibot.Utils.helpFormat import net.thauvin.erik.mobibot.Utils.helpFormat
@ -81,148 +81,6 @@ class Mobibot(nickname: String, val channel: String, logsDirPath: String, p: Pro
/** Logger. */ /** Logger. */
val logger: Logger = LoggerFactory.getLogger(Mobibot::class.java) 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 { companion object {
@JvmStatic @JvmStatic
@Throws(Exception::class) @Throws(Exception::class)
@ -254,7 +112,7 @@ class Mobibot(nickname: String, val channel: String, logsDirPath: String, p: Pro
if (version) { if (version) {
// Output the version // Output the version
println( println(
"${ReleaseInfo.PROJECT.capitalise()} ${ReleaseInfo.VERSION}" + "${ReleaseInfo.PROJECT.capitalize()} ${ReleaseInfo.VERSION}" +
" (${ReleaseInfo.BUILD_DATE.toIsoLocalDate()})" " (${ReleaseInfo.BUILD_DATE.toIsoLocalDate()})"
) )
println(ReleaseInfo.WEBSITE) println(ReleaseInfo.WEBSITE)
@ -416,5 +274,145 @@ class Mobibot(nickname: String, val channel: String, logsDirPath: String, p: Pro
// Sort the addons // Sort the addons
addons.names.sort() 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.* import java.util.*
/** /**
* Handles posts to pinboard.in. * Handles posts to `pinboard.in`.
*/ */
class Pinboard { class Pinboard {
private val poster = PinboardPoster() 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. * Deletes a pin.
*/ */
@ -73,6 +66,13 @@ class Pinboard {
} }
/**
* Sets the pinboard API token.
*/
fun setApiToken(apiToken: String) {
poster.apiToken = apiToken
}
/** /**
* Updates a pin. * 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. * Formats the tags for pinboard.
*/ */
@ -109,5 +100,14 @@ class Pinboard {
private fun EntryLink.postedBy(ircServer: String): String { private fun EntryLink.postedBy(ircServer: String): String {
return "Posted by $nick on $channel ( $ircServer )" 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 { object ReleaseInfo {
const val PROJECT = "mobibot" const val PROJECT = "mobibot"
const val VERSION = "0.8.0-rc+20250322004101" const val VERSION = "0.8.0-rc+20250507140607"
@JvmField @JvmField
@Suppress("MagicNumber") @Suppress("MagicNumber")
val BUILD_DATE: LocalDateTime = LocalDateTime.ofInstant( val BUILD_DATE: LocalDateTime = LocalDateTime.ofInstant(
Instant.ofEpochMilli(1742629261438L), ZoneId.systemDefault() Instant.ofEpochMilli(1746651967388L), ZoneId.systemDefault()
) )
const val WEBSITE = "https://mobitopia.org/mobibot/" const val WEBSITE = "https://mobitopia.org/mobibot/"

View file

@ -52,7 +52,7 @@ import kotlin.io.path.exists
import kotlin.io.path.fileSize import kotlin.io.path.fileSize
/** /**
* Miscellaneous utilities. * Miscellaneous utility functions.
*/ */
@Suppress("TooManyFunctions") @Suppress("TooManyFunctions")
object Utils { object Utils {
@ -111,13 +111,13 @@ object Utils {
* Capitalize a string. * Capitalize a string.
*/ */
@JvmStatic @JvmStatic
fun String.capitalise(): String = lowercase().replaceFirstChar { it.uppercase() } fun String.capitalize(): String = lowercase().replaceFirstChar { it.uppercase() }
/** /**
* Capitalize words * Capitalize words
*/ */
@JvmStatic @JvmStatic
fun String.capitalizeWords(): String = split(" ").joinToString(" ") { it.capitalise() } fun String.capitalizeWords(): String = split(" ").joinToString(" ") { it.capitalize() }
/** /**
* Colorize a string. * Colorize a string.
@ -204,7 +204,7 @@ object Utils {
fun Int.isHttpSuccess() = this in 200..399 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 @JvmStatic
fun List<String>.lastOrEmpty(): String { fun List<String>.lastOrEmpty(): String {
@ -261,6 +261,32 @@ object Utils {
return if (count > 1) "${this}s" else this 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. * Makes the given string red.
*/ */
@ -401,45 +427,19 @@ object Utils {
@JvmStatic @JvmStatic
fun LocalDateTime.toUtcDateTime(): String = format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")) fun LocalDateTime.toUtcDateTime(): String = format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))
/** /**
* Makes the given string bold. * Makes the given string bold.
*/ */
@JvmStatic @JvmStatic
fun String?.underline(): String = colorize(Colors.UNDERLINE) fun String?.underline(): String = colorize(Colors.UNDERLINE)
/** /**
* Converts XML/XHTML entities to plain text. * Converts XML/XHTML entities to plain text.
*/ */
@JvmStatic @JvmStatic
fun String.unescapeXml(): String = Jsoup.parse(this).text() 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. * 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.events.PrivateMessageEvent
import org.pircbotx.hooks.types.GenericMessageEvent 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 class AbstractCommand {
abstract val name: String abstract val name: String
abstract val help: List<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 net.thauvin.erik.mobibot.Utils.helpFormat
import org.pircbotx.hooks.types.GenericMessageEvent import org.pircbotx.hooks.types.GenericMessageEvent
/**
* Lists the last 5 posts from the channel's weblog feed.
*/
class ChannelFeed(channel: String) : AbstractCommand() { class ChannelFeed(channel: String) : AbstractCommand() {
override val name = channel override val name = channel
override val help = listOf("To list the last 5 posts from the channel's weblog feed:", helpFormat("%c $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 net.thauvin.erik.mobibot.Utils.isChannelOp
import org.pircbotx.hooks.types.GenericMessageEvent import org.pircbotx.hooks.types.GenericMessageEvent
/**
* Allows an operator to have the bot leave the channel and come back.
*/
class Cycle : AbstractCommand() { class Cycle : AbstractCommand() {
private val wait = 10 private val wait = 10
override val name = "cycle" 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 net.thauvin.erik.mobibot.Utils.isChannelOp
import org.pircbotx.hooks.types.GenericMessageEvent import org.pircbotx.hooks.types.GenericMessageEvent
/**
* Allows an operator to terminate the bot's operations on the server.
*/
class Die : AbstractCommand() { class Die : AbstractCommand() {
override val name = "die" override val name = "die"
override val help = emptyList<String>() override val help = emptyList<String>()
@ -42,6 +45,14 @@ class Die : AbstractCommand() {
override val isPublic = false override val isPublic = false
override val isVisible = 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) { override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
with(event.bot()) { with(event.bot()) {
if (event.isChannelOp(channel) && (properties[DIE_PROP].isNullOrBlank() || args == properties[DIE_PROP])) { 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 net.thauvin.erik.mobibot.commands.links.LinksManager
import org.pircbotx.hooks.types.GenericMessageEvent import org.pircbotx.hooks.types.GenericMessageEvent
/**
* Adds or removed nicks from the ignored list.
*/
class Ignore : AbstractCommand() { class Ignore : AbstractCommand() {
private val me = "me" 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 { init {
initProperties(IGNORE_PROP) initProperties(IGNORE_PROP)
} }
@ -65,17 +79,6 @@ class Ignore : AbstractCommand() {
override val isPublic = true override val isPublic = true
override val isVisible = 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) { override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
val isMe = args.trim().equals(me, true) val isMe = args.trim().equals(me, true)
if (isMe || !event.isChannelOp(channel)) { if (isMe || !event.isChannelOp(channel)) {

View file

@ -31,7 +31,7 @@
package net.thauvin.erik.mobibot.commands package net.thauvin.erik.mobibot.commands
import net.thauvin.erik.mobibot.ReleaseInfo 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.green
import net.thauvin.erik.mobibot.Utils.helpFormat import net.thauvin.erik.mobibot.Utils.helpFormat
import net.thauvin.erik.mobibot.Utils.isChannelOp import net.thauvin.erik.mobibot.Utils.isChannelOp
@ -46,9 +46,12 @@ import java.lang.management.ManagementFactory
import kotlin.time.DurationUnit import kotlin.time.DurationUnit
import kotlin.time.toDuration import kotlin.time.toDuration
/**
* Provides detailed bot and channel statistics.
*/
class Info(private val tell: Tell, private val seen: Seen) : AbstractCommand() { class Info(private val tell: Tell, private val seen: Seen) : AbstractCommand() {
private val allVersions = listOf( 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()})" "Written by ${ReleaseInfo.AUTHOR} (${ReleaseInfo.AUTHOR_URL.green()})"
) )
override val name = "info" override val name = "info"
@ -71,30 +74,32 @@ class Info(private val tell: Tell, private val seen: Seen) : AbstractCommand() {
val weeks = days / 7 val weeks = days / 7
days %= 7 days %= 7
with(StringBuffer()) { with(mutableListOf<String>()) {
if (years > 0) { if (years > 0) {
append(years).append(" year".plural(years)).append(' ') add("$years".plus(" year".plural(years)))
} }
if (months > 0) { if (months > 0) {
append(months).append(" month".plural(months)).append(' ') add("$months".plus(" month".plural(months)))
} }
if (weeks > 0) { if (weeks > 0) {
append(weeks).append(" week".plural(weeks)).append(' ') add("$weeks".plus(" week".plural(weeks)))
} }
if (days > 0) { if (days > 0) {
append(days).append(" day".plural(days)).append(' ') add("$days".plus(" day".plural(days)))
} }
if (hours > 0) { if (hours > 0) {
append(hours).append(" hour".plural(hours.toLong())).append(' ') add("$hours".plus(" hour".plural(hours.toLong())))
} }
if (minutes > 0) { if (minutes > 0) {
append(minutes).append(" minute".plural(minutes.toLong())) add("$minutes".plus(" minute".plural(minutes.toLong())))
} else { } else if (seconds > 0) {
append(seconds).append(" second".plural(seconds.toLong())) 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 net.thauvin.erik.mobibot.Utils.isChannelOp
import org.pircbotx.hooks.types.GenericMessageEvent import org.pircbotx.hooks.types.GenericMessageEvent
/**
* Allows an operator to have the bot perform an action in the specified channel.
*/
class Me : AbstractCommand() { class Me : AbstractCommand() {
override val name = "me" override val name = "me"
override val help = listOf("To have the bot perform an action:", helpFormat("%c $name <action>")) 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 net.thauvin.erik.mobibot.Utils.sendList
import org.pircbotx.hooks.types.GenericMessageEvent import org.pircbotx.hooks.types.GenericMessageEvent
/**
* List the enabled/disabled modules.
*/
class Modules(private val modules: List<String>, private val disabledModules: List<String>) : AbstractCommand() { class Modules(private val modules: List<String>, private val disabledModules: List<String>) : AbstractCommand() {
override val name = "modules" override val name = "modules"
override val help = listOf("To view a list of enabled/disabled modules:", helpFormat("%c $name")) 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 net.thauvin.erik.mobibot.Utils.isChannelOp
import org.pircbotx.hooks.types.GenericMessageEvent import org.pircbotx.hooks.types.GenericMessageEvent
/**
* Allows an operator to send a private message to the specified user.
*/
class Msg : AbstractCommand() { class Msg : AbstractCommand() {
override val name = "msg" override val name = "msg"
override val help = listOf( 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 net.thauvin.erik.mobibot.Utils.isChannelOp
import org.pircbotx.hooks.types.GenericMessageEvent import org.pircbotx.hooks.types.GenericMessageEvent
/**
* Allows an operator to change the bot's nickname.
*/
class Nick : AbstractCommand() { class Nick : AbstractCommand() {
override val name = "nick" override val name = "nick"
override val help = listOf("To change the bot's nickname:", helpFormat("%c $name <new_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.Clock
import java.time.LocalDateTime import java.time.LocalDateTime
/**
* Lists the last 10 public channel messages.
*/
class Recap : AbstractCommand() { class Recap : AbstractCommand() {
override val name = "recap" override val name = "recap"
override val help = listOf( 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 net.thauvin.erik.mobibot.Utils.isChannelOp
import org.pircbotx.hooks.types.GenericMessageEvent import org.pircbotx.hooks.types.GenericMessageEvent
/**
* Allows an operator to have the bot say something in the specified channel.
*/
class Say : AbstractCommand() { class Say : AbstractCommand() {
override val name = "say" override val name = "say"
override val help = listOf("To have the bot say something on the channel:", helpFormat("%c $name <text>")) 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 net.thauvin.erik.mobibot.Utils.sendList
import org.pircbotx.hooks.types.GenericMessageEvent import org.pircbotx.hooks.types.GenericMessageEvent
/**
* Lists the users present on the channel.
*/
class Users : AbstractCommand() { class Users : AbstractCommand() {
override val name = "users" override val name = "users"
override val help = listOf("To list the users present on the channel:", helpFormat("%c $name")) 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.PircBotX
import org.pircbotx.hooks.types.GenericMessageEvent import org.pircbotx.hooks.types.GenericMessageEvent
/**
* Lists the bot version, OS version, JVM version, Kotlin version and PircBotX version.
*/
class Versions : AbstractCommand() { class Versions : AbstractCommand() {
private val allVersions = listOf( private val allVersions = listOf(
"Version: ${ReleaseInfo.VERSION} (${ReleaseInfo.BUILD_DATE.toIsoLocalDate()})", "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 net.thauvin.erik.mobibot.entries.EntryLink
import org.pircbotx.hooks.types.GenericMessageEvent 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() { class Comment : AbstractCommand() {
override val name = COMMAND override val name = COMMAND
override val help = listOf( override val help = listOf(

View file

@ -49,6 +49,12 @@ import org.jsoup.Jsoup
import org.pircbotx.hooks.types.GenericMessageEvent import org.pircbotx.hooks.types.GenericMessageEvent
import java.io.IOException 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() { class LinksManager : AbstractCommand() {
private val defaultTags: MutableList<String> = mutableListOf() private val defaultTags: MutableList<String> = mutableListOf()
private val keywords: MutableList<String> = mutableListOf() private val keywords: MutableList<String> = mutableListOf()
@ -59,10 +65,6 @@ class LinksManager : AbstractCommand() {
override val isPublic = false override val isPublic = false
override val isVisible = false override val isVisible = false
init {
initProperties(TAGS_PROP, KEYWORDS_PROP)
}
companion object { companion object {
val LINK_MATCH = "^[hH][tT][tT][pP](|[sS])://.*".toRegex() val LINK_MATCH = "^[hH][tT][tT][pP](|[sS])://.*".toRegex()
const val KEYWORDS_PROP = "tags-keywords" 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) { override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
val cmds = args.split(" ".toRegex(), 2) val cmds = args.split(" ".toRegex(), 2)
val sender = event.user.nick val sender = event.user.nick
@ -120,10 +126,11 @@ class LinksManager : AbstractCommand() {
} }
if (title.isBlank()) { if (title.isBlank()) {
title = fetchTitle(link) title = fetchPageTitle(link)
} }
if (title != Constants.NO_TITLE) { if (title != Constants.NO_TITLE) {
// Add keywords as tags if found in the title
matchTagKeywords(title, tags) matchTagKeywords(title, tags)
} }
@ -158,7 +165,12 @@ class LinksManager : AbstractCommand() {
return message.matches(LINK_MATCH) 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 { try {
val html = Jsoup.connect(link) val html = Jsoup.connect(link)
.userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:74.0) Gecko/20100101 Firefox/74.0") .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>) { internal fun matchTagKeywords(title: String, tags: MutableList<String>) {
for (match in keywords) { for (match in keywords) {
val m = Regex.escape(match) 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 net.thauvin.erik.mobibot.entries.EntryLink
import org.pircbotx.hooks.types.GenericMessageEvent 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() { class Posting : AbstractCommand() {
override val name = "posting" override val name = "posting"
override val help = listOf( override val help = listOf(
@ -97,6 +102,20 @@ class Posting : AbstractCommand() {
entries.save() 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) { private fun changeTitle(cmd: String, entryIndex: Int, event: GenericMessageEvent) {
if (cmd.length > 1) { if (cmd.length > 1) {
val entry: EntryLink = entries.links[entryIndex] 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) { private fun removeEntry(channel: String, index: Int, event: GenericMessageEvent) {
val entry: EntryLink = entries.links[index] val entry: EntryLink = entries.links[index]
if (entry.login == event.user.login || event.isChannelOp(channel)) { 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 net.thauvin.erik.mobibot.entries.EntryLink
import org.pircbotx.hooks.types.GenericMessageEvent 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() { class Tags : AbstractCommand() {
override val name = COMMAND override val name = COMMAND
override val help = listOf( override val help = listOf(
@ -55,6 +61,7 @@ class Tags : AbstractCommand() {
const val COMMAND = "tags" const val COMMAND = "tags"
} }
override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) { override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
val cmds = args.substring(1).split("${Constants.TAG_CMD}:", limit = 2) val cmds = args.substring(1).split("${Constants.TAG_CMD}:", limit = 2)
val index = cmds[0].toInt() - 1 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.events.PrivateMessageEvent
import org.pircbotx.hooks.types.GenericMessageEvent import org.pircbotx.hooks.types.GenericMessageEvent
/**
* Displays a list of entries or an appropriate message if no entries exist.
*/
class View : AbstractCommand() { class View : AbstractCommand() {
override val name = VIEW_CMD override val name = VIEW_CMD
override val help = listOf( override val help = listOf(
@ -61,12 +64,17 @@ class View : AbstractCommand() {
override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) { override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
if (entries.links.isNotEmpty()) { if (entries.links.isNotEmpty()) {
val p = parseArgs(args) val p = parseArgs(args)
showPosts(p.first, p.second, event) viewEntries(p.first, p.second, event)
} else { } else {
event.sendMessage("There is currently nothing to view. Why don't you post something?") 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> { internal fun parseArgs(args: String): Pair<Int, String> {
var query = args.lowercase().trim() var query = args.lowercase().trim()
var start = 0 var start = 0
@ -88,7 +96,7 @@ class View : AbstractCommand() {
return Pair(start, query) 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 index = start
var entry: EntryLink var entry: EntryLink
var sent = 0 var sent = 0

View file

@ -33,13 +33,15 @@ package net.thauvin.erik.mobibot.commands.seen
import java.io.Serializable import java.io.Serializable
/**
* A comparator implementation for comparing nicknames in a case-insensitive manner.
*/
class NickComparator : Comparator<String>, Serializable { class NickComparator : Comparator<String>, Serializable {
override fun compare(a: String, b: String): Int { override fun compare(a: String, b: String): Int {
return a.lowercase().compareTo(b.lowercase()) return a.lowercase().compareTo(b.lowercase())
} }
companion object { companion object {
@Suppress("ConstPropertyName")
private const val serialVersionUID = 1L private const val serialVersionUID = 1L
} }
} }

View file

@ -49,7 +49,9 @@ import org.slf4j.Logger
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.util.* import java.util.*
/**
* Displays when a user was last seen, or all seen nicks.
*/
class Seen(private val serialObject: String) : AbstractCommand() { class Seen(private val serialObject: String) : AbstractCommand() {
private val logger: Logger = LoggerFactory.getLogger(Seen::class.java) private val logger: Logger = LoggerFactory.getLogger(Seen::class.java)
private val allKeyword = "all" private val allKeyword = "all"
@ -64,6 +66,9 @@ class Seen(private val serialObject: String) : AbstractCommand() {
override val isPublic = true override val isPublic = true
override val isVisible = true override val isVisible = true
init {
load()
}
override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) { override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
if (isEnabled()) { if (isEnabled()) {
@ -143,8 +148,4 @@ class Seen(private val serialObject: String) : AbstractCommand() {
fun save() { fun save() {
saveSerialData(serialObject, seenNicks, logger, "seen nicknames") 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 import java.io.Serializable
/**
* Holds a [Seen] nickname.
*/
data class SeenNick(val nick: String, val lastSeen: Long) : Serializable { data class SeenNick(val nick: String, val lastSeen: Long) : Serializable {
companion object { companion object {
@Suppress("ConstPropertyName")
private const val serialVersionUID = 1L private const val serialVersionUID = 1L
} }
} }

View file

@ -48,7 +48,7 @@ import org.pircbotx.hooks.types.GenericMessageEvent
import org.pircbotx.hooks.types.GenericUserEvent 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() { class Tell(private val serialObject: String) : AbstractCommand() {
// Messages queue // Messages queue

View file

@ -38,7 +38,7 @@ import java.time.Clock
import java.time.LocalDateTime import java.time.LocalDateTime
/** /**
* The Tell Messages Manager. * Manages the [Tell] messages queue.
*/ */
object TellManager { object TellManager {
private val logger: Logger = LoggerFactory.getLogger(TellManager::class.java) private val logger: Logger = LoggerFactory.getLogger(TellManager::class.java)

View file

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

View file

@ -33,6 +33,9 @@ package net.thauvin.erik.mobibot.entries
import net.thauvin.erik.mobibot.Utils.today import net.thauvin.erik.mobibot.Utils.today
/**
* Holds [EntryLink] entries.
*/
class Entries( class Entries(
var channel: String = "", var channel: String = "",
var ircServer: String = "", var ircServer: String = "",

View file

@ -35,7 +35,7 @@ import net.thauvin.erik.mobibot.Utils.bold
import net.thauvin.erik.mobibot.Utils.green import net.thauvin.erik.mobibot.Utils.green
/** /**
* Entries utilities. * Provides functions used to manage [Entries].
*/ */
object EntriesUtils { object EntriesUtils {
/** /**

View file

@ -34,7 +34,7 @@ import java.io.Serializable
import java.time.LocalDateTime import java.time.LocalDateTime
/** /**
* Entry comments data class. * [Entries] comment.
*/ */
data class EntryComment(var comment: String, var nick: String) : Serializable { 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 { companion object {
// Serial version UID // Serial version UID
@Suppress("ConstPropertyName")
private const val serialVersionUID: Long = 1L private const val serialVersionUID: Long = 1L
} }
} }

View file

@ -37,7 +37,7 @@ import java.io.Serializable
import java.util.* import java.util.*
/** /**
* The class used to store link entries. * Holds [Entries] link.
*/ */
class EntryLink( class EntryLink(
// Link's comments // Link's comments
@ -92,6 +92,11 @@ class EntryLink(
this.tags.addAll(tags) this.tags.addAll(tags)
} }
companion object {
// Serial version UID
private const val serialVersionUID: Long = 1L
}
/** /**
* Adds a new comment * Adds a new comment
*/ */
@ -204,10 +209,4 @@ class EntryLink(
return ("EntryLink{channel='$channel', comments=$comments, date=$date, link='$link', login='$login'," + return ("EntryLink{channel='$channel', comments=$comments, date=$date, link='$link', login='$login'," +
"nick='$nick', tags=$tags, title='$title'}") "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 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 { abstract class AbstractModule {
/** /**
@ -76,6 +78,7 @@ abstract class AbstractModule {
/** /**
* Responds with the module's help. * Responds with the module's help.
*/ */
@Suppress("SameReturnValue")
open fun helpResponse(event: GenericMessageEvent): Boolean { open fun helpResponse(event: GenericMessageEvent): Boolean {
for (h in help) { for (h in help) {
event.sendMessage(helpCmdSyntax(h, event.bot().nick, isPrivateMsgEnabled && event is PrivateMessageEvent)) 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 import java.text.DecimalFormat
/** /**
* The Calc module. * Performs a calculation.
*/ */
class Calc : AbstractModule() { class Calc : AbstractModule() {
private val logger: Logger = LoggerFactory.getLogger(Calc::class.java) private val logger: Logger = LoggerFactory.getLogger(Calc::class.java)
override val name = "Calc" 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 { companion object {
// Calc command // Calc command
private const val CALC_CMD = "calc" private const val CALC_CMD = "calc"
@ -84,4 +68,20 @@ class Calc : AbstractModule() {
help.add("To solve a mathematical calculation:") help.add("To solve a mathematical calculation:")
help.add(helpFormat("%c $CALC_CMD <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.Logger
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
/**
* Allows user to interact with ChatGPT.
*/
class ChatGpt2 : AbstractModule() { class ChatGpt2 : AbstractModule() {
val logger: Logger = LoggerFactory.getLogger(ChatGpt2::class.java) val logger: Logger = LoggerFactory.getLogger(ChatGpt2::class.java)
override val name = CHATGPT_NAME 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 { companion object {
/** /**
* The service name. * The service name.
@ -125,4 +102,33 @@ class ChatGpt2 : AbstractModule() {
} }
initProperties(API_KEY_PROP, MAX_TOKENS_PROP) 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 import java.io.IOException
/** /**
* The Cryptocurrency Prices module. * Retrieves cryptocurrency market prices.
*/ */
class CryptoPrices : AbstractModule() { class CryptoPrices : AbstractModule() {
private val logger: Logger = LoggerFactory.getLogger(CryptoPrices::class.java) private val logger: Logger = LoggerFactory.getLogger(CryptoPrices::class.java)
@ -102,7 +102,7 @@ class CryptoPrices : AbstractModule() {
private const val CODES_KEYWORD = "codes" private const val CODES_KEYWORD = "codes"
/** /**
* Get current market price. * Get the current market price.
*/ */
@JvmStatic @JvmStatic
fun currentPrice(args: List<String>): CryptoPrice { fun currentPrice(args: List<String>): CryptoPrice {

View file

@ -50,78 +50,13 @@ import java.util.*
/** /**
* The CurrencyConverter module. * Converts between currencies.
*/ */
class CurrencyConverter : AbstractModule() { class CurrencyConverter : AbstractModule() {
private val logger: Logger = LoggerFactory.getLogger(CurrencyConverter::class.java) private val logger: Logger = LoggerFactory.getLogger(CurrencyConverter::class.java)
override val name = "CurrencyConverter" 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 { companion object {
/** /**
* The API Key property. * The API Key property.
@ -219,4 +154,69 @@ class CurrencyConverter : AbstractModule() {
initProperties(API_KEY_PROP) initProperties(API_KEY_PROP)
loadSymbols(properties[ChatGpt2.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 import org.pircbotx.hooks.types.GenericMessageEvent
/** /**
* The Dice module. * Rolls dice.
*/ */
class Dice : AbstractModule() { class Dice : AbstractModule() {
override val name = "Dice" 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 { companion object {
// Dice command // Dice command
private const val DICE_CMD = "dice" private const val DICE_CMD = "dice"
@ -84,4 +73,15 @@ class Dice : AbstractModule() {
help.add("To roll 2 dice with 6 sides:") help.add("To roll 2 dice with 6 sides:")
help.add(helpFormat("%c $DICE_CMD [2d6]")) 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.pircbotx.hooks.types.GenericMessageEvent
import org.slf4j.Logger import org.slf4j.Logger
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.util.*
/**
* Allows user to interact with Gemini.
*/
class Gemini2 : AbstractModule() { class Gemini2 : AbstractModule() {
private val logger: Logger = LoggerFactory.getLogger(Gemini2::class.java) private val logger: Logger = LoggerFactory.getLogger(Gemini2::class.java)
override val name = GEMINI_NAME 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 { 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. * The service name.
*/ */
const val GEMINI_NAME = "Gemini" 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. * The max number of output tokens property.
@ -104,14 +92,10 @@ class Gemini2 : AbstractModule() {
return gemini.generate(query) return gemini.generate(query)
} catch (e: Exception) { } catch (e: Exception) {
throw ModuleException( throw ModuleException("$GEMINI_CMD($query): IO", IO_ERROR, e)
"$GEMINI_CMD($query): IO",
"An IO error has occurred while conversing with ${GEMINI_NAME}.",
e
)
} }
} else { } 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) 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 package net.thauvin.erik.mobibot.modules
import net.thauvin.erik.mobibot.ReleaseInfo 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.colorize
import net.thauvin.erik.mobibot.Utils.encodeUrl import net.thauvin.erik.mobibot.Utils.encodeUrl
import net.thauvin.erik.mobibot.Utils.helpFormat import net.thauvin.erik.mobibot.Utils.helpFormat
@ -51,43 +51,13 @@ import java.io.IOException
import java.net.URL import java.net.URL
/** /**
* The GoogleSearch module. * Allows user to search Google.
*/ */
class GoogleSearch : AbstractModule() { class GoogleSearch : AbstractModule() {
private val logger: Logger = LoggerFactory.getLogger(GoogleSearch::class.java) private val logger: Logger = LoggerFactory.getLogger(GoogleSearch::class.java)
override val name = "GoogleSearch" 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 { companion object {
// Google API Key property // Google API Key property
const val API_KEY_PROP = "google-api-key" const val API_KEY_PROP = "google-api-key"
@ -112,7 +82,7 @@ class GoogleSearch : AbstractModule() {
if (apiKey.isNullOrBlank() || cseKey.isNullOrBlank()) { if (apiKey.isNullOrBlank() || cseKey.isNullOrBlank()) {
throw ModuleException( throw ModuleException(
"${GoogleSearch::class.java.name} is disabled.", "${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>() val results = mutableListOf<Message>()
@ -159,4 +129,34 @@ class GoogleSearch : AbstractModule() {
help.add(helpFormat("%c $GOOGLE_CMD <query>")) help.add(helpFormat("%c $GOOGLE_CMD <query>"))
initProperties(API_KEY_PROP, CSE_KEY_PROP) 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 import java.io.IOException
/** /**
* The Joke module. * Displays jokes from [JokeAPI](https://v2.jokeapi.dev/).
*/ */
class Joke : AbstractModule() { class Joke : AbstractModule() {
private val logger: Logger = LoggerFactory.getLogger(Joke::class.java) private val logger: Logger = LoggerFactory.getLogger(Joke::class.java)
override val name = "Joke" 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 { companion object {
// Joke command // Joke command
private const val JOKE_CMD = "joke" private const val JOKE_CMD = "joke"
@ -102,4 +84,22 @@ class Joke : AbstractModule() {
help.add("To display a random joke:") help.add("To display a random joke:")
help.add(helpFormat("%c $JOKE_CMD")) 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 import java.net.UnknownHostException
/** /**
* The Lookup module. * Performs a DNS lookup or Whois IP query.
*/ */
class Lookup : AbstractModule() { class Lookup : AbstractModule() {
private val logger: Logger = LoggerFactory.getLogger(Lookup::class.java) private val logger: Logger = LoggerFactory.getLogger(Lookup::class.java)
override val name = "Lookup" 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 { companion object {
/** /**
* The whois default host. * The whois default host.
@ -168,4 +124,48 @@ class Lookup : AbstractModule() {
help.add("To perform a DNS lookup query:") help.add("To perform a DNS lookup query:")
help.add(helpFormat("%c $LOOKUP_CMD <ip address or hostname>")) 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.HttpRequest
import java.net.http.HttpResponse import java.net.http.HttpResponse
/**
* Allows users to post on Mastodon.
*/
class Mastodon : SocialModule() { class Mastodon : SocialModule() {
override val name = "Mastodon" override val name = "Mastodon"
@ -56,32 +59,6 @@ class Mastodon : SocialModule() {
override val isValidProperties: Boolean override val isValidProperties: Boolean
get() = !(properties[INSTANCE_PROP].isNullOrBlank() || properties[ACCESS_TOKEN_PROP].isNullOrBlank()) 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 { companion object {
// Property keys // Property keys
const val ACCESS_TOKEN_PROP = "mastodon-access-token" const val ACCESS_TOKEN_PROP = "mastodon-access-token"
@ -146,4 +123,30 @@ class Mastodon : SocialModule() {
properties[AUTO_POST_PROP] = "false" properties[AUTO_POST_PROP] = "false"
initProperties(ACCESS_TOKEN_PROP, HANDLE_PROP, INSTANCE_PROP) 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 package net.thauvin.erik.mobibot.modules
/**
* The `ModuleException` class.
*/
class ModuleException @JvmOverloads constructor( class ModuleException @JvmOverloads constructor(
val debugMessage: String, val debugMessage: String,
message: String? = null, message: String? = null,
cause: Throwable? = null cause: Throwable? = null
) : Exception(message, cause) { ) : Exception(message, cause) {
companion object { companion object {
@Suppress("ConstPropertyName")
private const val serialVersionUID = 1L private const val serialVersionUID = 1L
} }
} }

View file

@ -35,15 +35,11 @@ import net.thauvin.erik.mobibot.Utils.helpFormat
import org.pircbotx.hooks.types.GenericMessageEvent import org.pircbotx.hooks.types.GenericMessageEvent
/** /**
* The Ping module. * Responds with a random quirky response.
*/ */
class Ping : AbstractModule() { class Ping : AbstractModule() {
override val name = "Ping" override val name = "Ping"
override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
event.bot().sendIRC().action(channel, randomPing())
}
companion object { companion object {
/** /**
* The ping responses. * The ping responses.
@ -80,4 +76,8 @@ class Ping : AbstractModule() {
help.add("To ping the bot:") help.add("To ping the bot:")
help.add(helpFormat("%c $PING_CMD")) 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() { class RockPaperScissors : AbstractModule() {
override val name = "RockPaperScissors" 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 { init {
with(commands) { with(commands) {
add(Hands.ROCK.name.lowercase()) add(Hands.ROCK.name.lowercase())
@ -80,20 +94,6 @@ class RockPaperScissors : AbstractModule() {
abstract fun beats(hand: Hands): Boolean 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) { override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
val hand = Hands.valueOf(cmd.uppercase()) val hand = Hands.valueOf(cmd.uppercase())
val botHand = Hands.entries[(0..Hands.entries.size).random()] val botHand = Hands.entries[(0..Hands.entries.size).random()]

View file

@ -30,7 +30,7 @@
*/ */
package net.thauvin.erik.mobibot.modules 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.encodeUrl
import net.thauvin.erik.mobibot.Utils.helpFormat import net.thauvin.erik.mobibot.Utils.helpFormat
import net.thauvin.erik.mobibot.Utils.reader import net.thauvin.erik.mobibot.Utils.reader
@ -49,34 +49,13 @@ import java.io.IOException
import java.net.URL import java.net.URL
/** /**
* The StockQuote module. * Retrieves stock quotes from Alpha Vantage.
*/ */
class StockQuote : AbstractModule() { class StockQuote : AbstractModule() {
private val logger: Logger = LoggerFactory.getLogger(StockQuote::class.java) private val logger: Logger = LoggerFactory.getLogger(StockQuote::class.java)
override val name = "StockQuote" 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 { companion object {
/** /**
* The API property key. * The API property key.
@ -133,7 +112,7 @@ class StockQuote : AbstractModule() {
if (apiKey.isNullOrBlank()) { if (apiKey.isNullOrBlank()) {
throw ModuleException( throw ModuleException(
"${StockQuote::class.java.name} is disabled.", "${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>() val messages = mutableListOf<Message>()
@ -233,4 +212,25 @@ class StockQuote : AbstractModule() {
help.add(helpFormat("%c $STOCK_CMD <symbol|keywords>")) help.add(helpFormat("%c $STOCK_CMD <symbol|keywords>"))
initProperties(API_KEY_PROP) 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 import java.security.SecureRandom
/** /**
* The War module. * Plays the `war` card game.
*
* @author [Erik C. Thauvin](https://erik.thauvin.net/)
* @since 1.0
*/ */
class War : AbstractModule() { class War : AbstractModule() {
override val name = "War" 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( override fun commandResponse(
channel: String, cmd: String, args: String, channel: String, cmd: String, args: String,
event: GenericMessageEvent event: GenericMessageEvent
@ -66,24 +83,4 @@ class War : AbstractModule() {
) )
} while (i == y) } 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.core.OWM.Country
import net.aksingh.owmjapis.model.CurrentWeather import net.aksingh.owmjapis.model.CurrentWeather
import net.thauvin.erik.mobibot.Utils.bold 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.capitalizeWords
import net.thauvin.erik.mobibot.Utils.encodeUrl import net.thauvin.erik.mobibot.Utils.encodeUrl
import net.thauvin.erik.mobibot.Utils.helpFormat import net.thauvin.erik.mobibot.Utils.helpFormat
@ -51,38 +51,13 @@ import org.slf4j.LoggerFactory
import kotlin.math.roundToInt import kotlin.math.roundToInt
/** /**
* The `Weather2` module. * Retrieve weather information from OpenWeatherMap.
*/ */
class Weather2 : AbstractModule() { class Weather2 : AbstractModule() {
private val logger: Logger = LoggerFactory.getLogger(Weather2::class.java) private val logger: Logger = LoggerFactory.getLogger(Weather2::class.java)
override val name = "Weather" 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 { companion object {
/** /**
* The OpenWeatherMap API Key property. * The OpenWeatherMap API Key property.
@ -121,7 +96,7 @@ class Weather2 : AbstractModule() {
if (apiKey.isNullOrBlank()) { if (apiKey.isNullOrBlank()) {
throw ModuleException( throw ModuleException(
"${Weather2::class.java.name} is disabled.", "${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) val owm = OWM(apiKey)
@ -181,7 +156,7 @@ class Weather2 : AbstractModule() {
for (w in it) { for (w in it) {
w?.let { w?.let {
condition.append(' ') condition.append(' ')
.append(w.getDescription().capitalise()) .append(w.getDescription().capitalize())
.append('.') .append('.')
} }
} }
@ -247,4 +222,29 @@ class Weather2 : AbstractModule() {
} }
initProperties(API_KEY_PROP) 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" 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 { companion object {
/** /**
* The Wolfram Alpha API Key property. * The Wolfram Alpha API Key property.
@ -139,4 +105,41 @@ class WolframAlpha : AbstractModule() {
} }
initProperties(APPID_KEY_PROP, UNITS_PROP) 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) { override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
if (args.equals(ZONES_ARGS, true)) { if (args.equals(ZONES_ARGS, true)) {
event.sendMessage("The supported countries/zones are: ") event.sendMessage("The supported countries/zones are: ")
@ -377,14 +387,4 @@ class WorldTime : AbstractModule() {
} }
override val isPrivateMsgEnabled = true 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 package net.thauvin.erik.mobibot.msg
/** /**
* The `ErrorMessage` class. * Holds an error message.
*/ */
class ErrorMessage @JvmOverloads constructor(msg: String, color: String = DEFAULT_COLOR) : class ErrorMessage @JvmOverloads constructor(msg: String, color: String = DEFAULT_COLOR) :
Message(msg, color, isError = true) Message(msg, color, isError = true)

View file

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

View file

@ -31,7 +31,7 @@
package net.thauvin.erik.mobibot.msg package net.thauvin.erik.mobibot.msg
/** /**
* The `NoticeMessage` class. * Holds a notice message.
*/ */
class NoticeMessage @JvmOverloads constructor(msg: String, color: String = DEFAULT_COLOR) : class NoticeMessage @JvmOverloads constructor(msg: String, color: String = DEFAULT_COLOR) :
Message(msg, color, isNotice = true) Message(msg, color, isNotice = true)

View file

@ -30,8 +30,9 @@
*/ */
package net.thauvin.erik.mobibot.msg package net.thauvin.erik.mobibot.msg
/** /**
* The `PrivateMessage` class. * Holds a private message.
*/ */
class PrivateMessage @JvmOverloads constructor(msg: String, color: String = DEFAULT_COLOR) : class PrivateMessage @JvmOverloads constructor(msg: String, color: String = DEFAULT_COLOR) :
Message(msg, color, isPrivate = true) Message(msg, color, isPrivate = true)

View file

@ -31,6 +31,6 @@
package net.thauvin.erik.mobibot.msg 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) 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.* import java.util.*
/** /**
* Social Manager. * Manages social media modules and handles operations such as notifications, posting, and queuing entries.
*/ */
class SocialManager { class SocialManager {
private val entries: MutableSet<Int> = HashSet() 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.Logger
import org.slf4j.LoggerFactory 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() { abstract class SocialModule : AbstractModule() {
private val logger: Logger = LoggerFactory.getLogger(SocialManager::class.java) private val logger: Logger = LoggerFactory.getLogger(SocialManager::class.java)

View file

@ -33,6 +33,9 @@ package net.thauvin.erik.mobibot.social
import java.util.* import java.util.*
/**
* Timer used to post social entries.
*/
class SocialTimer(private var socialManager: SocialManager, private var index: Int) : TimerTask() { class SocialTimer(private var socialManager: SocialManager, private var index: Int) : TimerTask() {
override fun run() { override fun run() {
socialManager.postEntry(index) socialManager.postEntry(index)

View file

@ -53,7 +53,7 @@ class AddonsTest {
private val addons = Addons(p) private val addons = Addons(p)
@Test @Test
fun addTest() { fun `Add modules and commands`() {
// Modules // Modules
addons.add(Joke()) addons.add(Joke())
addons.add(RockPaperScissors()) addons.add(RockPaperScissors())

View file

@ -34,9 +34,6 @@ import org.junit.jupiter.api.extension.ExtendWith
/** /**
* Disables tests on CI annotation. * Disables tests on CI annotation.
*
* @author [Erik C. Thauvin](https://erik.thauvin.net/)
* @since 1.0
*/ */
@Target(AnnotationTarget.TYPE, AnnotationTarget.FUNCTION) @Target(AnnotationTarget.TYPE, AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME) @Retention(AnnotationRetention.RUNTIME)

View file

@ -36,9 +36,6 @@ import org.junit.jupiter.api.extension.ExtensionContext
/** /**
* Disables tests on CI condition. * Disables tests on CI condition.
*
* @author [Erik C. Thauvin](https://erik.thauvin.net/)
* @since 1.0
*/ */
class DisableOnCiCondition : ExecutionCondition { class DisableOnCiCondition : ExecutionCondition {
override fun evaluateExecutionCondition(context: ExtensionContext): ConditionEvaluationResult { override fun evaluateExecutionCondition(context: ExtensionContext): ConditionEvaluationResult {

View file

@ -37,39 +37,68 @@ import assertk.assertions.*
import com.rometools.rome.io.FeedException import com.rometools.rome.io.FeedException
import net.thauvin.erik.mobibot.FeedReader.Companion.readFeed import net.thauvin.erik.mobibot.FeedReader.Companion.readFeed
import net.thauvin.erik.mobibot.msg.Message 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.io.IOException
import java.net.MalformedURLException import java.net.MalformedURLException
import java.net.UnknownHostException import java.net.UnknownHostException
import kotlin.test.Test import kotlin.test.Test
class FeedReaderTest { class FeedReaderTest {
@Nested
@DisplayName("Failure Tests")
inner class FailureTests {
@Test @Test
fun readFeedTest() { fun invalidFeed() {
var messages = readFeed("https://feeds.thauvin.net/ethauvin") 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 { assertThat(messages, "messages").all {
size().isEqualTo(10) size().isEqualTo(10)
index(1).prop(Message::msg).contains("erik.thauvin.net") index(1).prop(Message::msg).contains("erik.thauvin.net")
} }
}
messages = readFeed("https://lorem-rss.herokuapp.com/feed?length=0") @Test
assertThat(messages, "messages").index(0).prop(Message::msg).contains("nothing") fun readThenValidateFeedContent() {
val messages = readFeed("https://lorem-rss.herokuapp.com/feed?length=84", 42)
messages = readFeed("https://lorem-rss.herokuapp.com/feed?length=84", 42)
assertThat(messages, "messages").size().isEqualTo(84) assertThat(messages, "messages").size().isEqualTo(84)
messages.forEachIndexed { i, m -> messages.forEachIndexed { i, m ->
if (i % 2 == 0) { if (i % 2 == 0) {
assertThat(m, "messages($i)").prop(Message::msg).startsWith("Lorem ipsum") assertThat(m, "messages($i)").prop(Message::msg).startsWith("Lorem ipsum")
} else { } else {
@Suppress("HttpUrlsUsage")
assertThat(m, "messages($i)").prop(Message::msg).contains("http://example.com/test/") 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.* import java.util.*
/** /**
* Access to `local.properties`. * Provides functions to access local properties and environment variables.
*/ */
open class LocalProperties { open class LocalProperties {
init { init {
@ -72,7 +72,7 @@ open class LocalProperties {
env?.let { env?.let {
localProps.setProperty(key, env) 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 import kotlin.test.assertTrue
class PinboardTest : LocalProperties() { class PinboardTest : LocalProperties() {
private val pinboard = Pinboard() private val apiToken = getProperty("pinboard-api-token")
@Test private val ircServer = "irc.test.com"
fun testPinboard() { private val pinboard = Pinboard().apply { setApiToken(apiToken) }
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"))
pinboard.setApiToken(apiToken) private fun newEntry(): EntryLink {
return EntryLink(randomUrl(), "Test Example", "ErikT", "", "#mobitopia", listOf("test"))
}
pinboard.addPin(ircServer, entry) private fun randomUrl(): String {
assertTrue(validatePin(apiToken, url = entry.link, entry.title, entry.nick, entry.channel), "addPin") return "https://www.example.com/${(5001..9999).random()}"
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 validatePin(apiToken: String, url: String, vararg matches: String): Boolean { private fun validatePin(apiToken: String, url: String, vararg matches: String): Boolean {
@ -78,4 +65,43 @@ class PinboardTest : LocalProperties() {
return response.contains(url) 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 assertk.assertions.length
import net.thauvin.erik.mobibot.Utils.appendIfMissing import net.thauvin.erik.mobibot.Utils.appendIfMissing
import net.thauvin.erik.mobibot.Utils.bold 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.capitalizeWords
import net.thauvin.erik.mobibot.Utils.colorize import net.thauvin.erik.mobibot.Utils.colorize
import net.thauvin.erik.mobibot.Utils.cyan 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.Utils.unescapeXml
import net.thauvin.erik.mobibot.msg.Message.Companion.DEFAULT_COLOR import net.thauvin.erik.mobibot.msg.Message.Companion.DEFAULT_COLOR
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Nested
import org.pircbotx.Colors import org.pircbotx.Colors
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
@ -71,137 +73,221 @@ import kotlin.test.Test
class UtilsTest { class UtilsTest {
private val ascii = private val ascii =
" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~" " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"
private val cal = Calendar.getInstance() private val p = Properties().apply {
private val localDateTime = LocalDateTime.of(1952, 2, 17, 12, 30, 0) setProperty("one", "1")
setProperty("two", "two")
}
private val test = "This is a test." 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 @BeforeEach
fun setUp() { fun beforeEach() {
cal[1952, Calendar.FEBRUARY, 17, 12, 30] = 0 cal[1952, Calendar.FEBRUARY, 17, 12, 30] = 0
} }
@Test @Test
fun testAppendIfMissing() { fun `Convert a Date to an ISO date`() {
val dir = "dir" assertThat(cal.time.toIsoLocalDate(), "isoLocalDate(date)").isEqualTo("1952-02-17")
val sep = '/'
val url = "https://erik.thauvin.net"
assertThat(dir.appendIfMissing(File.separatorChar), "appendIfMissing(dir)")
.isEqualTo(dir + File.separatorChar)
assertThat(url.appendIfMissing(sep), "appendIfMissing(url)").isEqualTo("$url$sep")
assertThat("$url$sep".appendIfMissing(sep), "appendIfMissing($url$sep)").isEqualTo("$url$sep")
} }
@Test @Test
fun testBold() { fun `Convert a LocalDate to an ISO date`() {
assertThat(1.bold(), "bold(1)").isEqualTo(Colors.BOLD + "1" + Colors.BOLD) assertThat(localDateTime.toIsoLocalDate(), "isoLocalDate(localDate)").isEqualTo("1952-02-17")
assertThat(2L.bold(), "bold(2L)").isEqualTo(Colors.BOLD + "2" + Colors.BOLD)
assertThat(ascii.bold(), "ascii.bold()").isEqualTo(Colors.BOLD + ascii + Colors.BOLD)
assertThat("test".bold(), "test.bold()").isEqualTo(Colors.BOLD + "test" + Colors.BOLD)
}
@Test
fun testCapitalise() {
assertThat("test".capitalise(), "capitalize(test)").isEqualTo("Test")
assertThat("Test".capitalise(), "capitalize(Test)").isEqualTo("Test")
assertThat(test.capitalise(), "capitalize($test)").isEqualTo(test)
assertThat("".capitalise(), "capitalize()").isEqualTo("")
} }
@Test @Test
fun textCapitaliseWords() { fun `Convert a Date to a UTC date-time`() {
assertThat(test.capitalizeWords(), "captiatlizeWords(test)").isEqualTo("This Is A Test.") assertThat(cal.time.toUtcDateTime(), "utcDateTime(date)").isEqualTo("1952-02-17 12:30")
assertThat("Already Capitalized".capitalizeWords(), "already capitalized")
.isEqualTo("Already Capitalized")
assertThat(" a test ".capitalizeWords(), "with spaces").isEqualTo(" A Test ")
} }
@Test @Test
fun testColorize() { fun `Convert a LocalDate to a UTC date-time`() {
assertThat(ascii.colorize(Colors.REVERSE), "reverse.colorize()").isEqualTo( assertThat(localDateTime.toUtcDateTime(), "utcDateTime(localDate)").isEqualTo("1952-02-17 12:30")
Colors.REVERSE + ascii + Colors.REVERSE
)
assertThat(ascii.colorize(Colors.RED), "red.colorize()")
.isEqualTo(Colors.RED + ascii + Colors.NORMAL)
assertThat(ascii.colorize(Colors.BOLD), "colorized(bold)")
.isEqualTo(Colors.BOLD + ascii + Colors.BOLD)
assertThat(null.colorize(Colors.RED), "null.colorize()").isEqualTo("")
assertThat("".colorize(Colors.RED), "colorize()").isEqualTo("")
assertThat(ascii.colorize(DEFAULT_COLOR), "ascii.colorize()").isEqualTo(ascii)
assertThat(" ".colorize(Colors.NORMAL), "blank.colorize()")
.isEqualTo(Colors.NORMAL + " " + Colors.NORMAL)
} }
@Test @Test
fun testCyan() { fun `Today should return the current date in ISO format`() {
assertThat(ascii.cyan()).isEqualTo(Colors.CYAN + ascii + Colors.NORMAL) assertThat(today()).isEqualTo(LocalDateTime.now().toIsoLocalDate())
}
} }
@Test @Nested
fun testEncodeUrl() { @DisplayName("Help Tests")
assertThat("Hello Günter".encodeUrl()).isEqualTo("Hello%20G%C3%BCnter") inner class HelpTests {
} private val bot = "mobibot"
@Test @Test
fun testGetIntProperty() { fun `Construct help string for public message`() {
val p = Properties()
p["one"] = "1"
p["two"] = "two"
assertThat(p.getIntProperty("one", 9), "getIntProperty(one)").isEqualTo(1)
assertThat(p.getIntProperty("two", 2), "getIntProperty(two)").isEqualTo(2)
assertThat(p.getIntProperty("foo", 3), "getIntProperty(foo)").isEqualTo(3)
}
@Test
fun testGreen() {
assertThat(ascii.green()).isEqualTo(Colors.DARK_GREEN + ascii + Colors.NORMAL)
}
@Test
fun testHelpCmdSyntax() {
val bot = "mobibot"
assertThat(helpCmdSyntax("%c $test %n $test", bot, false), "helpCmdSyntax(private)")
.isEqualTo("$bot: $test $bot $test")
assertThat(helpCmdSyntax("%c %n $test %c $test %n", bot, true), "helpCmdSyntax(public)") assertThat(helpCmdSyntax("%c %n $test %c $test %n", bot, true), "helpCmdSyntax(public)")
.isEqualTo("/msg $bot $bot $test /msg $bot $test $bot") .isEqualTo("/msg $bot $bot $test /msg $bot $test $bot")
} }
@Test @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)") assertThat(helpFormat(test, isBold = true, isIndent = false), "helpFormat(bold)")
.isEqualTo("${Colors.BOLD}$test${Colors.BOLD}") .isEqualTo("${Colors.BOLD}$test${Colors.BOLD}")
}
@Test
fun `Format help string with indent`() {
assertThat(helpFormat(test, isBold = false, isIndent = true), "helpFormat(indent)") assertThat(helpFormat(test, isBold = false, isIndent = true), "helpFormat(indent)")
.isEqualTo(test.prependIndent()) .isEqualTo(test.prependIndent())
}
@Test
fun `Format help string with bold and indent`() {
assertThat(helpFormat(test, isBold = true, isIndent = true), "helpFormat(bold,indent)") assertThat(helpFormat(test, isBold = true, isIndent = true), "helpFormat(bold,indent)")
.isEqualTo(test.colorize(Colors.BOLD).prependIndent()) .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 @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") val two = listOf("1", "2")
assertThat(two.lastOrEmpty(), "lastOrEmpty(1,2)").isEqualTo("2") assertThat(two.lastOrEmpty(), "lastOrEmpty(1,2)").isEqualTo("2")
}
@Test
fun `Return empty if list only has one item`() {
val one = listOf("1") val one = listOf("1")
assertThat(one.lastOrEmpty(), "lastOrEmpty(1)").isEqualTo("") 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 @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 { assertThat(ascii.obfuscate(), "obfuscate()").all {
length().isEqualTo(ascii.length) length().isEqualTo(ascii.length)
isEqualTo(("x".repeat(ascii.length))) isEqualTo(("x".repeat(ascii.length)))
} }
assertThat(" ".obfuscate(), "obfuscate(blank)").isEqualTo(" ")
} }
@Test @Test
fun testPlural() { fun `Obfuscate empty string`() {
assertThat(" ".obfuscate(), "obfuscate(blank)").isEqualTo(" ")
}
}
@Test
fun `Pluralize string`() {
val week = "week" val week = "week"
val weeks = "weeks" 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 @Test
fun testReplaceEach() { fun `Replace occurrences in string`() {
val search = arrayOf("one", "two", "three")
val replace = arrayOf("1", "2", "3")
assertThat(search.joinToString(",").replaceEach(search, replace), "replaceEach(1,2,3") assertThat(search.joinToString(",").replaceEach(search, replace), "replaceEach(1,2,3")
.isEqualTo(replace.joinToString(",")) .isEqualTo(replace.joinToString(","))
}
@Test
fun `Replace occurrences not found in string`() {
assertThat(test.replaceEach(search, replace), "replaceEach(nothing)").isEqualTo(test) 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)") assertThat(test.replaceEach(arrayOf("t", "e"), arrayOf("", "E")), "replaceEach($test)")
.isEqualTo(test.replace("t", "").replace("e", "E")) .isEqualTo(test.replace("t", "").replace("e", "E"))
}
@Test
fun `Replace empty occurrences in string`() {
assertThat(test.replaceEach(search, emptyArray()), "replaceEach(search, empty)") assertThat(test.replaceEach(search, emptyArray()), "replaceEach(search, empty)")
.isEqualTo(test) .isEqualTo(test)
} }
@Test
fun testRed() {
assertThat(ascii.red()).isEqualTo(ascii.colorize(Colors.RED))
} }
@Test @Test
fun testReverseColor() { fun `Unescape XML`() {
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() {
assertThat("&lt;a name=&quot;test &amp; &apos;&#39;&quot;&gt;".unescapeXml()).isEqualTo( assertThat("&lt;a name=&quot;test &amp; &apos;&#39;&quot;&gt;".unescapeXml()).isEqualTo(
"<a name=\"test & ''\">" "<a name=\"test & ''\">"
) )
} }
}
@Test @Test
@Throws(IOException::class) @Throws(IOException::class)
fun testUrlReader() { fun `URL reader`() {
val reader = URL("https://postman-echo.com/status/200").reader() val reader = URL("https://postman-echo.com/status/200").reader()
assertThat(reader.body).isEqualTo("{\n \"status\": 200\n}") assertThat(reader.body).isEqualTo("{\n \"status\": 200\n}")
assertThat(reader.responseCode).isEqualTo(200) assertThat(reader.responseCode).isEqualTo(200)
} }
@Nested
@DisplayName("Text Styling Tests")
inner class TextStylingTests {
@Nested
@DisplayName("Colorize Tests")
inner class ColorizeTests {
@Test @Test
fun testUtcDateTime() { fun `Colorize ASCII characters red`() {
assertThat(cal.time.toUtcDateTime(), "utcDateTime(date)").isEqualTo("1952-02-17 12:30") assertThat(ascii.colorize(Colors.RED), "red.colorize()")
assertThat(localDateTime.toUtcDateTime(), "utcDateTime(localDate)").isEqualTo("1952-02-17 12:30") .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.assertThat
import assertk.assertions.isEqualTo import assertk.assertions.isEqualTo
import net.thauvin.erik.mobibot.commands.Info.Companion.toUptime 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 import kotlin.test.Test
class InfoTest { class InfoTest {
@Nested
@DisplayName("Uptime Tests")
inner class UptimeTests {
@Test @Test
fun testToUptime() { fun `Years, Months, Weeks, Days, Hours and Minutes`() {
assertThat( assertThat(547800300076L.toUptime()).isEqualTo("17 years 4 months 2 weeks 1 day 6 hours 45 minutes")
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")
@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 { class RecapTest {
@Test @Test
fun storeRecapTest() { fun storeRecap() {
for (i in 1..20) { for (i in 1..20) {
Recap.storeRecap("sender$i", "test $i", false) Recap.storeRecap("sender$i", "test $i", false)
} }
@ -54,7 +54,7 @@ class RecapTest {
} }
Recap.storeRecap("sender", "test action", true) 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()) .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.all
import assertk.assertThat import assertk.assertThat
import assertk.assertions.contains import assertk.assertions.*
import assertk.assertions.isEqualTo
import assertk.assertions.isTrue
import assertk.assertions.size
import net.thauvin.erik.mobibot.Constants import net.thauvin.erik.mobibot.Constants
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Nested
import kotlin.test.Test import kotlin.test.Test
class LinksManagerTest { class LinksManagerTest {
private val linksManager = LinksManager() private val linksManager = LinksManager()
@Nested
@DisplayName("Fetch Page Title Tests")
inner class FetchPageTitleTests {
@Test @Test
fun fetchTitle() { fun fetchPageTitle() {
assertThat(linksManager.fetchTitle("https://erik.thauvin.net/"), "fetchTitle(Erik)").contains("Erik's Weblog") assertThat(linksManager.fetchPageTitle("https://erik.thauvin.net/")).contains("Erik's Weblog")
assertThat(
linksManager.fetchTitle("https://www.google.com/foo"),
"fetchTitle(Foo)"
).isEqualTo(Constants.NO_TITLE)
} }
@Test @Test
fun testMatches() { fun fetchPageNoTitle() {
assertThat(linksManager.matches("https://www.example.com/"), "matches(url)").isTrue() assertThat(linksManager.fetchPageTitle("https://www.google.com/foo")).isEqualTo(Constants.NO_TITLE)
assertThat(linksManager.matches("HTTP://erik.thauvin.net/blog/ Erik's Weblog"), "matches(HTTP)").isTrue() }
}
@Nested
@DisplayName("Match Tests")
inner class MatchTests {
@Nested
@DisplayName("Link Tests")
inner class LinkTests {
@Test
@Suppress("HttpUrlsUsage")
fun matchInsecureLink() {
assertThat(linksManager.matches("http://erik.thauvin.net/blog/ Erik's Weblog")).isTrue()
} }
@Test @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") linksManager.setProperty(LinksManager.KEYWORDS_PROP, "key1 key2,key3")
val tags = mutableListOf<String>() val tags = mutableListOf<String>()
linksManager.matchTagKeywords("Test title with key2", tags) linksManager.matchTagKeywords("Test title with key2", tags)
assertThat(tags, "tags").contains("key2") assertThat(tags, "tags").containsExactly("key2")
tags.clear() }
@Test
fun matchTagKeywords() {
val tags = mutableListOf("key1", "key3")
linksManager.matchTagKeywords("Test key3 title with key1", tags) linksManager.matchTagKeywords("Test key3 title with key1", tags)
assertThat(tags, "tags(key1, key3)").all { assertThat(tags, "tags(key1, key3)").all {
contains("key1") containsExactlyInAnyOrder("key1", "key3")
contains("key3")
size().isEqualTo(2) size().isEqualTo(2)
} }
} }
}
}
} }

View file

@ -36,13 +36,13 @@ import assertk.assertThat
import assertk.assertions.isEqualTo import assertk.assertions.isEqualTo
import assertk.assertions.prop import assertk.assertions.prop
import net.thauvin.erik.mobibot.entries.EntryLink import net.thauvin.erik.mobibot.entries.EntryLink
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Nested
import kotlin.test.Test import kotlin.test.Test
class ViewTest { class ViewTest {
@Test companion object {
fun testParseArgs() { init {
val view = View()
for (i in 1..10) { for (i in 1..10) {
LinksManager.entries.links.add( LinksManager.entries.links.add(
EntryLink( 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 { @Nested
prop(Pair<Int, String>::first).isEqualTo(1) @DisplayName("Parse Args Tests")
prop(Pair<Int, String>::second).isEqualTo("foo") inner class ParseArgsTests {
} private val view = View()
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")
}
@Test
fun `Parse alphanumeric query`() {
assertThat(view.parseArgs("1a"), "parseArgs(1a)").all { assertThat(view.parseArgs("1a"), "parseArgs(1a)").all {
prop(Pair<Int, String>::first).isEqualTo(0) prop(Pair<Int, String>::first).isEqualTo(0)
prop(Pair<Int, String>::second).isEqualTo("1a") 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 { assertThat(view.parseArgs(""), "parseArgs()").all {
prop(Pair<Int, String>::first).isEqualTo(LinksManager.entries.links.size - View.MAX_ENTRIES) prop(Pair<Int, String>::first).isEqualTo(LinksManager.entries.links.size - View.MAX_ENTRIES)
prop(Pair<Int, String>::second).isEqualTo("") prop(Pair<Int, String>::second).isEqualTo("")
} }
}
LinksManager.entries.links.clear() @Test
fun `Parse first item`() {
assertThat(view.parseArgs("4"), "parseArgs(4)").all { assertThat(view.parseArgs("1"), "parseArgs(1)").all {
prop(Pair<Int, String>::first).isEqualTo(0) prop(Pair<Int, String>::first).isEqualTo(0)
prop(Pair<Int, String>::second).isEqualTo("") 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.all
import assertk.assertThat import assertk.assertThat
import assertk.assertions.* import assertk.assertions.*
import org.junit.AfterClass import org.junit.jupiter.api.MethodOrderer
import org.junit.BeforeClass
import org.junit.jupiter.api.Order import org.junit.jupiter.api.Order
import kotlin.io.path.deleteIfExists import org.junit.jupiter.api.TestMethodOrder
import kotlin.io.path.fileSize
import kotlin.test.Test import kotlin.test.Test
@TestMethodOrder(MethodOrderer.OrderAnnotation::class)
class SeenTest { 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 @Test
@Order(1) @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() seen.clear()
assertThat(seen::seenNicks).isEmpty() assertThat(seen::seenNicks).isEmpty()
seen.load() seen.load()
assertThat(seen::seenNicks).key(NICK).isNotNull() 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 @Test
@Order(3) @Order(3)
fun clearTest() { fun clear() {
seen.clear() seen.clear()
seen.save() seen.save()
seen.load() seen.load()
assertThat(seen::seenNicks).size().isEqualTo(0) assertThat(seen::seenNicks).isEmpty()
}
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()
}
} }
} }

View file

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

View file

@ -34,10 +34,8 @@ package net.thauvin.erik.mobibot.commands.tell
import assertk.all import assertk.all
import assertk.assertThat import assertk.assertThat
import assertk.assertions.* import assertk.assertions.*
import org.junit.AfterClass
import java.time.LocalDateTime import java.time.LocalDateTime
import kotlin.io.path.createTempFile import kotlin.io.path.createTempFile
import kotlin.io.path.deleteIfExists
import kotlin.io.path.fileSize import kotlin.io.path.fileSize
import kotlin.test.Test 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 { init {
TellManager.save(testFile.toAbsolutePath().toString(), testMessages) TellManager.save(testFile.toAbsolutePath().toString(), testMessages)
assertThat(testFile.fileSize()).isGreaterThan(0) assertThat(testFile.fileSize()).isGreaterThan(0)
} }
@Test @Test
fun cleanTest() { fun clean() {
testMessages.add(TellMessage("sender", "recipient", "message").apply { testMessages.add(TellMessage("sender", "recipient", "message").apply {
queued = LocalDateTime.now().minusDays(maxDays) queued = LocalDateTime.now().minusDays(maxDays)
}) })
@ -66,7 +72,7 @@ class TellMessagesMgrTest {
} }
@Test @Test
fun loadTest() { fun load() {
val messages = TellManager.load(testFile.toAbsolutePath().toString()) val messages = TellManager.load(testFile.toAbsolutePath().toString())
for (i in messages.indices) { for (i in messages.indices) {
assertThat(messages).index(i).all { 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.printLink
import net.thauvin.erik.mobibot.entries.EntriesUtils.printTags import net.thauvin.erik.mobibot.entries.EntriesUtils.printTags
import net.thauvin.erik.mobibot.entries.EntriesUtils.toLinkLabel import net.thauvin.erik.mobibot.entries.EntriesUtils.toLinkLabel
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Nested
import kotlin.test.Test import kotlin.test.Test
class EntriesUtilsTest { class EntriesUtilsTest {
@ -58,13 +60,16 @@ class EntriesUtilsTest {
} }
} }
@Nested
@DisplayName("Print Tests")
inner class PrintTests {
@Test @Test
fun printCommentTest() { fun printComment() {
assertThat(printComment(0, 0, comment)).isEqualTo("${Constants.LINK_CMD}1.1: [nick] comment") assertThat(printComment(0, 0, comment)).isEqualTo("${Constants.LINK_CMD}1.1: [nick] comment")
} }
@Test @Test
fun printLinkTest() { fun printLink() {
for (i in links.indices) { for (i in links.indices) {
assertThat( assertThat(
printLink(i - 1, links[i]), "link $i" printLink(i - 1, links[i]), "link $i"
@ -76,16 +81,17 @@ class EntriesUtilsTest {
} }
@Test @Test
fun printTagsTest() { fun printTags() {
for (i in links.indices) { for (i in links.indices) {
assertThat( assertThat(
printTags(i - 1, links[i]), "tag $i" printTags(i - 1, links[i]), "tag $i"
).isEqualTo("L${i}T: tag1, tag2, tag3, tag4, tag5") ).isEqualTo("L${i}T: tag1, tag2, tag3, tag4, tag5")
} }
} }
}
@Test @Test
fun toLinkLabelTest() { fun toLinkLabel() {
assertThat(1.toLinkLabel()).isEqualTo("${Constants.LINK_CMD}2") assertThat(1.toLinkLabel()).isEqualTo("${Constants.LINK_CMD}2")
} }
} }

View file

@ -35,6 +35,8 @@ import assertk.assertThat
import assertk.assertions.* import assertk.assertions.*
import com.rometools.rome.feed.synd.SyndCategory import com.rometools.rome.feed.synd.SyndCategory
import com.rometools.rome.feed.synd.SyndCategoryImpl 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.security.SecureRandom
import java.util.* import java.util.*
import kotlin.test.Test import kotlin.test.Test
@ -46,7 +48,7 @@ class EntryLinkTest {
) )
@Test @Test
fun testAddDeleteComment() { fun `Add then delete comment`() {
var i = 0 var i = 0
while (i < 5) { while (i < 5) {
entryLink.addComment("c$i", "u$i") entryLink.addComment("c$i", "u$i")
@ -63,7 +65,7 @@ class EntryLinkTest {
} }
val r = SecureRandom() val r = SecureRandom()
while (entryLink.comments.size > 0) { while (entryLink.comments.isNotEmpty()) {
entryLink.deleteComment(r.nextInt(entryLink.comments.size)) entryLink.deleteComment(r.nextInt(entryLink.comments.size))
} }
assertThat(entryLink.comments, "hasComments()").isEmpty() assertThat(entryLink.comments, "hasComments()").isEmpty()
@ -79,7 +81,7 @@ class EntryLinkTest {
} }
@Test @Test
fun testConstructor() { fun `Validate EntryLink constructor`() {
val tags = listOf(SyndCategoryImpl().apply { name = "tag1" }, SyndCategoryImpl().apply { name = "tag2" }) val tags = listOf(SyndCategoryImpl().apply { name = "tag1" }, SyndCategoryImpl().apply { name = "tag2" })
val link = EntryLink("link", "title", "nick", "channel", Date(), tags) val link = EntryLink("link", "title", "nick", "channel", Date(), tags)
assertThat(link, "link").all { assertThat(link, "link").all {
@ -89,7 +91,7 @@ class EntryLinkTest {
} }
@Test @Test
fun testMatches() { fun `Validate EntryLink matches`() {
assertThat(entryLink.matches("mobitopia"), "matches(mobitopia)").isTrue() assertThat(entryLink.matches("mobitopia"), "matches(mobitopia)").isTrue()
assertThat(entryLink.matches("skynx"), "match(nick)").isTrue() assertThat(entryLink.matches("skynx"), "match(nick)").isTrue()
assertThat(entryLink.matches("www.mobitopia.org"), "matches(url)").isTrue() assertThat(entryLink.matches("www.mobitopia.org"), "matches(url)").isTrue()
@ -98,29 +100,52 @@ class EntryLinkTest {
assertThat(entryLink.matches(null), "matches(null)").isFalse() assertThat(entryLink.matches(null), "matches(null)").isFalse()
} }
@Nested
@DisplayName("Validate Tags Test")
inner class ValidateTagsTest {
@Test @Test
fun testTags() { fun `Validate tags parsing in constructor`() {
val tags: List<SyndCategory> = entryLink.tags val tags: List<SyndCategory> = entryLink.tags
for ((i, tag) in tags.withIndex()) { for ((i, tag) in tags.withIndex()) {
assertThat(tag.name, "tag.name($i)").isEqualTo("tag${i + 1}") assertThat(tag.name, "tag.name($i)").isEqualTo("tag${i + 1}")
} }
assertThat(entryLink::tags).size().isEqualTo(5) 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( assertThat(
entryLink.formatTags(","), link.formatTags(",")
"formatTags(',')" ).isEqualTo("tag1,tag2,tag3,tag4,tag5,mobitopia")
).isEqualTo("tag1,tag2,tag3,tag4,mobitopia") }
entryLink.setTags("-tag4 tag5")
@Test
fun `Validate formatting tags with spaces`() {
val link = entryLink
link.setTags("-tag4")
assertThat( assertThat(
entryLink.formatTags(" ", ","), "formatTag(' ',',')" link.formatTags(" ", ",")
).isEqualTo(",tag1 tag2 tag3 mobitopia tag5") ).isEqualTo(",tag1 tag2 tag3 tag5")
val size = entryLink.tags.size }
entryLink.setTags("")
assertThat(entryLink.tags, "setTags('')").size().isEqualTo(size) @Test
entryLink.setTags(" ") fun `Validate setting blank tags`() {
assertThat(entryLink.tags, "setTags(' ')").size().isEqualTo(size) 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.assertThat
import assertk.assertions.* import assertk.assertions.*
import net.thauvin.erik.mobibot.Utils.today 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.nio.file.Paths
import java.util.* import java.util.*
import kotlin.io.path.deleteIfExists import kotlin.io.path.deleteIfExists
@ -42,19 +46,26 @@ import kotlin.io.path.fileSize
import kotlin.io.path.name import kotlin.io.path.name
import kotlin.test.Test import kotlin.test.Test
@TestMethodOrder(MethodOrderer.OrderAnnotation::class)
class FeedMgrTest { class FeedMgrTest {
private val entries = Entries() private val entries = Entries()
private val channel = "mobibot" private val channel = "mobibot"
private var currentFile: Path
private var backlogFile: Path
init { init {
entries.logsDir = "src/test/resources/" entries.logsDir = "src/test/resources/"
entries.ircServer = "irc.example.com" entries.ircServer = "irc.example.com"
entries.channel = channel entries.channel = channel
entries.backlogs = "https://www.mobitopia.org/mobibot/logs" entries.backlogs = "https://www.mobitopia.org/mobibot/logs"
currentFile = Paths.get("${entries.logsDir}test.xml")
backlogFile = Paths.get("${entries.logsDir}${today()}.xml")
} }
@Test @Test
fun testFeedMgr() { @Order(1)
fun loadFeed() {
// Load the feed // Load the feed
assertThat(FeedsManager.loadFeed(entries), "loadFeed()").isEqualTo("2021-10-31") assertThat(FeedsManager.loadFeed(entries), "loadFeed()").isEqualTo("2021-10-31")
@ -87,26 +98,33 @@ class FeedMgrTest {
prop(EntryLink::nick).isEqualTo("Skynx") prop(EntryLink::nick).isEqualTo("Skynx")
prop(EntryLink::date).isEqualTo(Date(1635638460000L)) prop(EntryLink::date).isEqualTo(Date(1635638460000L))
} }
}
val currentFile = Paths.get("${entries.logsDir}test.xml") @Test
val backlogFile = Paths.get("${entries.logsDir}${today()}.xml") @Order(2)
fun saveFeed() {
// Save the feed
FeedsManager.saveFeed(entries, currentFile.name) FeedsManager.saveFeed(entries, currentFile.name)
assertThat(currentFile, "currentFile").exists() assertThat(currentFile, "currentFile").exists()
assertThat(backlogFile, "backlogFile").exists() assertThat(backlogFile, "backlogFile").exists()
assertThat(currentFile.fileSize(), "currentFile == backlogFile").isEqualTo(backlogFile.fileSize()) assertThat(currentFile.fileSize(), "currentFile == backlogFile").isEqualTo(backlogFile.fileSize())
}
// Load the test feed @Test
@Order(3)
fun loadTestFeed() {
entries.links.clear() entries.links.clear()
FeedsManager.loadFeed(entries, currentFile.name) FeedsManager.loadFeed(entries, currentFile.name)
entries.links.forEachIndexed { i, entryLink -> entries.links.forEachIndexed { i, entryLink ->
assertThat(entryLink.title, "entryLink.title[${i + 1}]").isEqualTo("Example ${i + 1}") assertThat(entryLink.title, "entryLink.title[${i + 1}]").isEqualTo("Example ${i + 1}")
} }
}
@Test
@Order(4)
fun deleteFeeds() {
assertThat(currentFile.deleteIfExists(), "currentFile.deleteIfExists()").isTrue() assertThat(currentFile.deleteIfExists(), "currentFile.deleteIfExists()").isTrue()
assertThat(backlogFile.deleteIfExists(), "backlogFile.deleteIfExists()").isTrue() assertThat(backlogFile.deleteIfExists(), "backlogFile.deleteIfExists()").isTrue()
} }

View file

@ -41,10 +41,33 @@ import kotlin.test.Test
class CalcTest { class CalcTest {
@Test @Test
fun testCalculate() { fun `Calculate basic addition`() {
assertThat(calculate("1 + 1"), "calculate(1+1)").isEqualTo("1+1 = ${2.bold()}") 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()}") 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()}") 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) 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.assertFailure
import assertk.assertThat import assertk.assertThat
import assertk.assertions.contains
import assertk.assertions.hasNoCause import assertk.assertions.hasNoCause
import assertk.assertions.isInstanceOf import assertk.assertions.isInstanceOf
import assertk.assertions.contains
import net.thauvin.erik.mobibot.DisableOnCi import net.thauvin.erik.mobibot.DisableOnCi
import net.thauvin.erik.mobibot.LocalProperties import net.thauvin.erik.mobibot.LocalProperties
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Nested
import kotlin.test.Test import kotlin.test.Test
class ChatGpt2Test : LocalProperties() { class ChatGpt2Test : LocalProperties() {
@Test @Test
fun testApiKey() { fun apiKey() {
assertFailure { ChatGpt2.chat("1 gallon to liter", "", 0) } assertFailure { ChatGpt2.chat("1 gallon to liter", "", 0) }
.isInstanceOf(ModuleException::class.java) .isInstanceOf(ModuleException::class.java)
.hasNoCause() .hasNoCause()
} }
@Nested
@DisplayName("Chat Tests")
inner class ChatTests {
private val apiKey = getProperty(ChatGpt2.API_KEY_PROP)
@Test @Test
@DisableOnCi @DisableOnCi
fun testChat() { fun chat() {
val apiKey = getProperty(ChatGpt2.API_KEY_PROP)
assertThat( assertThat(
ChatGpt2.chat("how do I make an HTTP request in Javascript?", apiKey, 200) ChatGpt2.chat(
).contains("XMLHttpRequest") "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) } assertFailure { ChatGpt2.chat("1 liter to gallon", apiKey, -1) }
.isInstanceOf(ModuleException::class.java) .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.getCurrencyName
import net.thauvin.erik.mobibot.modules.CryptoPrices.Companion.loadCurrencies import net.thauvin.erik.mobibot.modules.CryptoPrices.Companion.loadCurrencies
import org.junit.jupiter.api.BeforeAll 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.ConsoleHandler
import java.util.logging.Level import java.util.logging.Level
import kotlin.test.Test import kotlin.test.Test
@ -49,30 +51,6 @@ class CryptoPricesTest {
loadCurrencies() 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 { companion object {
@JvmStatic @JvmStatic
@BeforeAll @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.ErrorMessage
import net.thauvin.erik.mobibot.msg.Message import net.thauvin.erik.mobibot.msg.Message
import net.thauvin.erik.mobibot.msg.PublicMessage import net.thauvin.erik.mobibot.msg.PublicMessage
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Nested
import kotlin.test.Test import kotlin.test.Test
class CurrencyConverterTest : LocalProperties() { class CurrencyConverterTest : LocalProperties() {
@ -50,28 +52,49 @@ class CurrencyConverterTest : LocalProperties() {
loadSymbols(apiKey) loadSymbols(apiKey)
} }
@Nested
@DisplayName("Currency Converter Tests")
inner class CurrencyConverterTests {
private val apiKey = getProperty(CurrencyConverter.API_KEY_PROP)
@Test @Test
fun testConvertCurrency() { fun `Convert CAD to USD`() {
val apiKey = getProperty(CurrencyConverter.API_KEY_PROP)
assertThat(
convertCurrency(apiKey, "100 USD to EUR").msg,
"convertCurrency(100 USD to EUR)"
).matches("100 United States Dollar = \\d{2,3}\\.\\d{2,3} Euro".toRegex())
assertThat(
convertCurrency(apiKey, "1 USD to GBP").msg,
"convertCurrency(1 USD to BGP)"
).matches("1 United States Dollar = 0\\.\\d{2,3} Pound Sterling".toRegex())
assertThat( assertThat(
convertCurrency(apiKey, "100,000.00 CAD to USD").msg, convertCurrency(apiKey, "100,000.00 CAD to USD").msg,
"convertCurrency(100,000.00 GBP to USD)" "convertCurrency(100,000.00 GBP to USD)"
).matches("100,000.00 Canadian Dollar = \\d+\\.\\d{2,3} United States Dollar".toRegex()) ).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 { assertThat(convertCurrency(apiKey, "100 USD to USD"), "convertCurrency(100 USD to USD)").all {
prop(Message::msg).contains("You're kidding, right?") prop(Message::msg).contains("You're kidding, right?")
isInstanceOf(PublicMessage::class.java) isInstanceOf(PublicMessage::class.java)
} }
}
@Test
fun `Invalid Query should throw exception`() {
assertThat(convertCurrency(apiKey, "100 USD"), "convertCurrency(100 USD)").all { assertThat(convertCurrency(apiKey, "100 USD"), "convertCurrency(100 USD)").all {
prop(Message::msg).contains("Invalid query.") prop(Message::msg).contains("Invalid query.")
isInstanceOf(ErrorMessage::class.java) isInstanceOf(ErrorMessage::class.java)
} }
} }
}
} }

View file

@ -35,19 +35,64 @@ package net.thauvin.erik.mobibot.modules
import assertk.assertThat import assertk.assertThat
import assertk.assertions.isEqualTo import assertk.assertions.isEqualTo
import assertk.assertions.matches 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 import kotlin.test.Test
class DiceTest { class DiceTest {
@Nested
@DisplayName("Roll Tests")
inner class RollTests {
@Test @Test
fun testRoll() { fun `Roll 1 die with 1 side`() {
assertThat(Dice.roll(1, 1), "roll(1d1)").isEqualTo("\u00021\u0002") assertThat(Dice.roll(1, 1)).isEqualTo("\u00021\u0002")
assertThat(Dice.roll(2, 1), "roll(2d1)") }
.isEqualTo("\u00021\u0002 + \u00021\u0002 = \u00022\u0002")
assertThat(Dice.roll(5, 1), "roll(5d1)") @Test
.isEqualTo("\u00021\u0002 + \u00021\u0002 + \u00021\u0002 + \u00021\u0002 + \u00021\u0002 = \u00025\u0002") fun `Roll 1 die with 6 sides`() {
assertThat(Dice.roll(2, 6), "roll(2d6)") 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()) .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 assertk.assertions.*
import net.thauvin.erik.mobibot.DisableOnCi import net.thauvin.erik.mobibot.DisableOnCi
import net.thauvin.erik.mobibot.LocalProperties import net.thauvin.erik.mobibot.LocalProperties
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Nested
import kotlin.test.Test import kotlin.test.Test
class Gemini2Test : LocalProperties() { 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 @Test
fun testApiKey() { @DisableOnCi
assertFailure { Gemini2.chat("1 gallon to liter", "", 0) } fun chatHttpRequestInJavascript() {
.isInstanceOf(ModuleException::class.java) assertThat(
.hasNoCause() Gemini2.chat(
"return only the code for a javascript function to make a request with XMLHttpRequest",
apiKey,
maxTokens
)
).isNotNull().contains("```javascript")
} }
@Test @Test
@DisableOnCi @DisableOnCi
fun chatPrompt() { fun chatEncodeUrlInJava() {
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")
assertThat( assertThat(
Gemini2.chat("how do I encode a URL in java?", apiKey, 60) Gemini2.chat("how do I encode a URL in java?", apiKey, 60)
).isNotNull().contains("URLEncoder") ).isNotNull().contains("URLEncoder")
}
}
@Nested
@DisplayName("API Keys Test")
inner class ApiKeysTest {
@Test
fun invalidApiKey() {
assertFailure { Gemini2.chat("1 liter to gallon", "foo", 40) } assertFailure { Gemini2.chat("1 liter to gallon", "foo", 40) }
.isInstanceOf(ModuleException::class.java) .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.modules.GoogleSearch.Companion.searchGoogle
import net.thauvin.erik.mobibot.msg.ErrorMessage import net.thauvin.erik.mobibot.msg.ErrorMessage
import net.thauvin.erik.mobibot.msg.Message import net.thauvin.erik.mobibot.msg.Message
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Nested
import kotlin.test.Test import kotlin.test.Test
class GoogleSearchTest : LocalProperties() { 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) @Throws(ModuleException::class)
fun testSearchGoogle() { fun sanitizedSearch(query: String, apiKey: String, cseKey: String): List<Message> {
val apiKey = getProperty(GoogleSearch.API_KEY_PROP)
val cseKey = getProperty(GoogleSearch.CSE_KEY_PROP)
try { try {
var query = "mobibot" return searchGoogle(query, apiKey, cseKey)
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.")
}
} catch (e: ModuleException) { } catch (e: ModuleException) {
// Avoid displaying api keys in CI logs // Avoid displaying api keys in CI logs
if ("true" == System.getenv("CI")) { 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 { class JokeTest {
@Test @Test
@Throws(ModuleException::class) @Throws(ModuleException::class)
fun testRandomJoke() { fun `Get a random joke`() {
val joke = randomJoke() val joke = randomJoke()
assertThat(joke, "randomJoke()").all { assertThat(joke, "randomJoke()").all {
size().isGreaterThan(0) size().isGreaterThan(0)

View file

@ -35,22 +35,32 @@ import assertk.assertions.any
import assertk.assertions.contains import assertk.assertions.contains
import net.thauvin.erik.mobibot.modules.Lookup.Companion.nslookup import net.thauvin.erik.mobibot.modules.Lookup.Companion.nslookup
import net.thauvin.erik.mobibot.modules.Lookup.Companion.whois 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 import kotlin.test.Test
class LookupTest { class LookupTest {
@Nested
@DisplayName("Lookup Tests")
inner class LookupTests {
@Test @Test
@Throws(Exception::class) @Throws(Exception::class)
fun testLookup() { fun lookupByHostname() {
var result = nslookup("apple.com") val result = nslookup("apple.com")
assertThat(result, "lookup(apple.com)").contains("17.253.144.10") 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 @Test
@Throws(Exception::class) @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) val result = whois("17.178.96.59", Lookup.WHOIS_HOST)
assertThat(result, "whois(17.178.96.59").any { it.contains("Apple Inc.") } 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() { class MastodonTest : LocalProperties() {
@Test @Test
@Throws(ModuleException::class) @Throws(ModuleException::class)
fun testToot() { fun `Toot on Mastodon`() {
val msg = "Testing Mastodon API from ${getHostName()}" val msg = "Testing Mastodon API from ${getHostName()}"
assertThat( assertThat(
toot( toot(

View file

@ -34,6 +34,8 @@ import assertk.all
import assertk.assertThat import assertk.assertThat
import assertk.assertions.* import assertk.assertions.*
import net.thauvin.erik.mobibot.ExceptionSanitizer.sanitize 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.ParameterizedTest
import org.junit.jupiter.params.provider.MethodSource import org.junit.jupiter.params.provider.MethodSource
import java.io.IOException import java.io.IOException
@ -45,29 +47,32 @@ class ModuleExceptionTest {
const val MESSAGE = "message" const val MESSAGE = "message"
@JvmStatic @JvmStatic
fun dataProviders(): List<ModuleException> { fun moduleExceptions(): List<ModuleException> {
return listOf( return listOf(
ModuleException(DEBUG_MESSAGE, MESSAGE, IOException("URL http://foobar.com")), ModuleException(DEBUG_MESSAGE, MESSAGE, IOException("foo")),
ModuleException(DEBUG_MESSAGE, MESSAGE, IOException("URL http://foobar.com?")), ModuleException(DEBUG_MESSAGE, MESSAGE, IllegalArgumentException("bar")),
ModuleException(DEBUG_MESSAGE, MESSAGE) ModuleException(DEBUG_MESSAGE, MESSAGE)
) )
} }
} }
@Nested
@DisplayName("Message Tests")
inner class MessageTests {
@ParameterizedTest @ParameterizedTest
@MethodSource("dataProviders") @MethodSource("net.thauvin.erik.mobibot.modules.ModuleExceptionTest#moduleExceptions")
fun testGetDebugMessage(e: ModuleException) { fun getDebugMessage(e: ModuleException) {
assertThat(e::debugMessage).isEqualTo(DEBUG_MESSAGE) assertThat(e::debugMessage).isEqualTo(DEBUG_MESSAGE)
} }
@ParameterizedTest @ParameterizedTest
@MethodSource("dataProviders") @MethodSource("net.thauvin.erik.mobibot.modules.ModuleExceptionTest#moduleExceptions")
fun testGetMessage(e: ModuleException) { fun getMessage(e: ModuleException) {
assertThat(e).hasMessage(MESSAGE) assertThat(e).hasMessage(MESSAGE)
} }
@Test @Test
fun testSanitizeMessage() { fun sanitizeMessage() {
val apiKey = "1234567890" val apiKey = "1234567890"
var e = ModuleException(DEBUG_MESSAGE, MESSAGE, IOException("URL http://foo.com?apiKey=$apiKey&userID=me")) var e = ModuleException(DEBUG_MESSAGE, MESSAGE, IOException("URL http://foo.com?apiKey=$apiKey&userID=me"))
assertThat( assertThat(
@ -101,4 +106,5 @@ class ModuleExceptionTest {
} }
assertThat(e.sanitize(), "exception should be unchanged").isEqualTo(e) assertThat(e.sanitize(), "exception should be unchanged").isEqualTo(e)
} }
}
} }

View file

@ -38,14 +38,14 @@ import kotlin.test.Test
class PingTest { class PingTest {
@Test @Test
fun testPingsArray() { fun `Get a radon ping`() {
assertThat(Ping.PINGS, "Ping.PINGS").isNotEmpty()
}
@Test
fun testRandomPing() {
for (i in 0..9) { for (i in 0..9) {
assertThat(Ping.PINGS, "Ping.PINGS[$i]").contains(randomPing()) 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.assertThat
import assertk.assertions.isEqualTo import assertk.assertions.isEqualTo
import net.thauvin.erik.mobibot.modules.RockPaperScissors.Companion.winLoseOrDraw 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 import kotlin.test.Test
class RockPaperScissorsTest { class RockPaperScissorsTest {
@Nested
@DisplayName("Win, Lose or Draw Tests")
inner class WinLoseOrDrawTests {
@Test @Test
fun testWinLoseOrDraw() { fun `Paper versus Paper draws`() {
assertThat(winLoseOrDraw("scissors", "paper"), "scissors vs. paper").isEqualTo("win") assertThat(winLoseOrDraw("paper", "paper")).isEqualTo("draw")
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") @Test
assertThat(winLoseOrDraw("rock", "paper"), "rock vs. paper").isEqualTo("lose") fun `Paper versus Rock wins`() {
assertThat(winLoseOrDraw("scissors", "rock"), "scissors vs. rock").isEqualTo("lose") assertThat(winLoseOrDraw("paper", "rock")).isEqualTo("win")
assertThat(winLoseOrDraw("scissors", "scissors"), "scissors vs. scissors").isEqualTo("draw") }
@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