Compare commits

...

5 commits

106 changed files with 3292 additions and 1717 deletions

7
.idea/kotlinc.xml generated
View file

@ -7,10 +7,7 @@
<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.1" />
<option name="languageVersion" value="2.0" /> <option name="languageVersion" value="2.1" />
</component>
<component name="KotlinJpsPluginSettings">
<option name="version" value="2.1.0" />
</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>
@ -89,14 +84,16 @@
<ID>TooManyFunctions:Tell.kt$Tell : AbstractCommand</ID> <ID>TooManyFunctions:Tell.kt$Tell : AbstractCommand</ID>
<ID>UtilityClassWithPublicConstructor:LocalProperties.kt$LocalProperties</ID> <ID>UtilityClassWithPublicConstructor:LocalProperties.kt$LocalProperties</ID>
<ID>WildcardImport:AddonsTest.kt$import net.thauvin.erik.mobibot.modules.*</ID> <ID>WildcardImport:AddonsTest.kt$import net.thauvin.erik.mobibot.modules.*</ID>
<ID>WildcardImport:CryptoPricesTest.kt$import assertk.assertions.*</ID>
<ID>WildcardImport:CurrencyConverterTest.kt$import assertk.assertions.*</ID>
<ID>WildcardImport:EntryLinkTest.kt$import assertk.assertions.*</ID> <ID>WildcardImport:EntryLinkTest.kt$import assertk.assertions.*</ID>
<ID>WildcardImport:FeedMgrTest.kt$import assertk.assertions.*</ID> <ID>WildcardImport:FeedMgrTest.kt$import assertk.assertions.*</ID>
<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+20250509073816</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;
autoDownloadPurge = true; autoDownloadPurge = true;
downloadSources = true;
repositories = List.of( repositories = List.of(
MAVEN_LOCAL, MAVEN_LOCAL,
MAVEN_CENTRAL, MAVEN_CENTRAL,
@ -117,14 +118,19 @@ public class MobibotBuild extends Project {
.include(dependency("net.aksingh", "owm-japis", "2.5.3.0")) .include(dependency("net.aksingh", "owm-japis", "2.5.3.0"))
.include(dependency("net.objecthunter", "exp4j", "0.4.8")) .include(dependency("net.objecthunter", "exp4j", "0.4.8"))
.include(dependency("org.json", "json", "20250107")) .include(dependency("org.json", "json", "20250107"))
.include(dependency("org.jsoup", "jsoup", "1.19.1")) .include(dependency("org.jsoup", "jsoup", "1.20.1"))
// Thauvin // Thauvin
.include(dependency("net.thauvin.erik", "cryptoprice", "1.0.3-SNAPSHOT")) .include(dependency("net.thauvin.erik", "cryptoprice", "1.0.3-SNAPSHOT"))
.include(dependency("net.thauvin.erik", "jokeapi", "1.0.1-SNAPSHOT")) .include(dependency("net.thauvin.erik", "jokeapi", "1.0.1-SNAPSHOT"))
.include(dependency("net.thauvin.erik", "pinboard-poster", "1.2.1-SNAPSHOT")) .include(dependency("net.thauvin.erik", "pinboard-poster", "1.2.1-SNAPSHOT"))
.include(dependency("net.thauvin.erik.urlencoder", "urlencoder-lib-jvm", "1.6.0")); .include(dependency("net.thauvin.erik.urlencoder", "urlencoder-lib-jvm", "1.6.0"));
scope(test) scope(test)
// Mockito
.include(dependency("net.bytebuddy", "byte-buddy", version(1, 17, 5)))
.include(dependency("org.mockito.kotlin", "mockito-kotlin", version(5, 4, 0)))
// AssertK
.include(dependency("com.willowtreeapps.assertk", "assertk-jvm", version(0, 28, 1))) .include(dependency("com.willowtreeapps.assertk", "assertk-jvm", version(0, 28, 1)))
// JUnit
.include(dependency("org.jetbrains.kotlin", "kotlin-test-junit5", kotlin)) .include(dependency("org.jetbrains.kotlin", "kotlin-test-junit5", kotlin))
.include(dependency("org.junit.jupiter", "junit-jupiter", version(5, 12, 2))) .include(dependency("org.junit.jupiter", "junit-jupiter", version(5, 12, 2)))
.include(dependency("org.junit.platform", "junit-platform-console-standalone", version(1, 12, 2))) .include(dependency("org.junit.platform", "junit-platform-console-standalone", version(1, 12, 2)))
@ -166,10 +172,9 @@ public class MobibotBuild extends Project {
@Override @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().languageVersion("2.1").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.
*/ */
@ -80,7 +76,7 @@ object Constants {
const val NO_TITLE = "No Title" const val NO_TITLE = "No Title"
/** /**
* Properties command line argument. * `Properties` command line argument.
*/ */
const val PROPS_ARG = "properties" const val PROPS_ARG = "properties"
@ -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)
@ -267,10 +125,10 @@ class Mobibot(nickname: String, val channel: String, logsDirPath: String, p: Pro
).use { fis -> ).use { fis ->
p.load(fis) p.load(fis)
} }
} catch (ignore: FileNotFoundException) { } catch (_: FileNotFoundException) {
System.err.println("Unable to find properties file.") System.err.println("Unable to find properties file.")
exitProcess(1) exitProcess(1)
} catch (ignore: IOException) { } catch (_: IOException) {
System.err.println("Unable to open properties file.") System.err.println("Unable to open properties file.")
exitProcess(1) exitProcess(1)
} }
@ -289,7 +147,7 @@ class Mobibot(nickname: String, val channel: String, logsDirPath: String, p: Pro
), true ), true
) )
System.setOut(stdout) System.setOut(stdout)
} catch (ignore: IOException) { } catch (_: IOException) {
System.err.println("Unable to open output (stdout) log file.") System.err.println("Unable to open output (stdout) log file.")
exitProcess(1) exitProcess(1)
} }
@ -300,7 +158,7 @@ class Mobibot(nickname: String, val channel: String, logsDirPath: String, p: Pro
), true ), true
) )
System.setErr(stderr) System.setErr(stderr)
} catch (ignore: IOException) { } catch (_: IOException) {
System.err.println("Unable to open error (stderr) log file.") System.err.println("Unable to open error (stderr) log file.")
exitProcess(1) exitProcess(1)
} }
@ -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+20250509075545"
@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(1746802545281L), 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.
*/ */
@ -305,7 +331,7 @@ object Utils {
} }
/** /**
* Send a formatted commands/modules, etc. list. * Send a formatted list to the channel.
*/ */
@JvmStatic @JvmStatic
@JvmOverloads @JvmOverloads
@ -368,7 +394,7 @@ object Utils {
fun String.toIntOrDefault(defaultValue: Int): Int { fun String.toIntOrDefault(defaultValue: Int): Int {
return try { return try {
toInt() toInt()
} catch (e: NumberFormatException) { } catch (_: NumberFormatException) {
defaultValue defaultValue
} }
} }
@ -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)
} }
@ -139,7 +146,7 @@ class LinksManager : AbstractCommand() {
pinboard.addPin(event.bot().serverHostname, entry) pinboard.addPin(event.bot().serverHostname, entry)
// Queue link for posting to social media. // Queue the entry for posting to social media.
socialManager.queueEntry(index) socialManager.queueEntry(index)
entries.save() entries.save()
@ -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")
@ -167,7 +179,7 @@ class LinksManager : AbstractCommand() {
if (title.isNotBlank()) { if (title.isNotBlank()) {
return title return title
} }
} catch (ignore: IOException) { } catch (_: IOException) {
// Do nothing // Do nothing
} }
return Constants.NO_TITLE return Constants.NO_TITLE
@ -181,12 +193,16 @@ class LinksManager : AbstractCommand() {
"Duplicate".bold() + " >> " + printLink(entries.links.indexOf(match), match) "Duplicate".bold() + " >> " + printLink(entries.links.indexOf(match), match)
) )
true true
} catch (ignore: NoSuchElementException) { } catch (_: NoSuchElementException) {
false false
} }
} }
} }
/**
* 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
@ -81,14 +89,14 @@ class View : AbstractCommand() {
if (start > entries.links.size) { if (start > entries.links.size) {
start = 0 start = 0
} }
} catch (ignore: NumberFormatException) { } catch (_: NumberFormatException) {
// Do nothing // Do nothing
} }
} }
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,16 @@ 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") @Suppress("unused")
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,12 @@ 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") @Suppress("unused")
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
@ -284,7 +284,7 @@ class Tell(private val serialObject: String) : AbstractCommand() {
// Arrow // Arrow
private const val ARROW = " --> " private const val ARROW = " --> "
// All keyword // The `all` keyword
private const val TELL_ALL_KEYWORD = "all" private const val TELL_ALL_KEYWORD = "all"
// The delete command. // The delete command.

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,7 @@ class TellMessage(
} }
companion object { companion object {
@Suppress("ConstPropertyName") @Suppress("unused")
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 {
/** /**
@ -69,14 +69,14 @@ object EntriesUtils {
} }
/** /**
* Prints an entry's tags/categories for display on the channel. e.g. L1T: tag1, tag2 * Prints an entry's tags/categories for display on the channel. (e.g., L1T: tag1, tag2)
*/ */
@JvmStatic @JvmStatic
fun printTags(entryIndex: Int, entry: EntryLink): String = fun printTags(entryIndex: Int, entry: EntryLink): String =
entryIndex.toLinkLabel() + "${Constants.TAG_CMD}: " + entry.formatTags(", ") entryIndex.toLinkLabel() + "${Constants.TAG_CMD}: " + entry.formatTags(", ")
/** /**
* Builds link label based on its index. e.g: L1 * Builds link label based on its index. (e.g., L1)
*/ */
@JvmStatic @JvmStatic
fun Int.toLinkLabel(): String = Constants.LINK_CMD + (this + 1) fun Int.toLinkLabel(): String = Constants.LINK_CMD + (this + 1)

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 {
/** /**
@ -45,8 +45,7 @@ data class EntryComment(var comment: String, var nick: String) : Serializable {
override fun toString(): String = "EntryComment{comment='$comment', date=$date, nick='$nick'}" override fun toString(): String = "EntryComment{comment='$comment', date=$date, nick='$nick'}"
companion object { companion object {
// Serial version UID @Suppress("unused")
@Suppress("ConstPropertyName") private const val serialVersionUID = 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 {
@Suppress("unused")
private const val serialVersionUID = 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,35 +40,19 @@ 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"
/** /**
* Performs a calculation. e.g.: 1 + 1 * 2 * Performs a calculation (e.g.: 1 + 1 * 2)
*/ */
@JvmStatic @JvmStatic
@Throws(IllegalArgumentException::class) @Throws(IllegalArgumentException::class)
@ -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: UnknownFunctionOrVariableException) {
if (logger.isWarnEnabled) logger.warn("Unable to calculate: $args", e)
event.respond("No idea. This is the kind of math I don't get.")
} catch (e: IllegalArgumentException) {
if (logger.isWarnEnabled) logger.warn("Failed to calculate: $args", e)
event.respond("No idea. I must've some form of Dyscalculia.")
}
} else {
helpResponse(event)
}
}
} }

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.
@ -82,7 +59,7 @@ class ChatGpt2 : AbstractModule() {
const val API_KEY_PROP = "chatgpt-api-key" const val API_KEY_PROP = "chatgpt-api-key"
/** /**
* The max tokens property. * The max-tokens property.
*/ */
const val MAX_TOKENS_PROP = "chatgpt-max-tokens" const val MAX_TOKENS_PROP = "chatgpt-max-tokens"
@ -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,54 +43,13 @@ 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)
override val name = "CryptoPrices" override val name = "CryptoPrices"
/**
* Returns the cryptocurrency market price from
* [Coinbase](https://docs.cdp.coinbase.com/coinbase-app/docs/api-prices#get-spot-price).
*/
override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
if (CURRENCIES.isEmpty()) {
try {
loadCurrencies()
} catch (e: ModuleException) {
if (logger.isWarnEnabled) logger.warn(e.debugMessage, e)
}
}
val debugMessage = "crypto($cmd $args)"
if (args == CODES_KEYWORD) {
event.sendMessage("The supported currencies are:")
event.sendList(ArrayList(CURRENCIES.keys), 10, isIndent = true)
} else if (args.matches("\\w+( [a-zA-Z]{3}+)?".toRegex())) {
try {
val price = currentPrice(args.split(' '))
val amount = try {
price.toCurrency()
} catch (ignore: IllegalArgumentException) {
price.amount
}
event.respond("${price.base} current price is $amount [${CURRENCIES[price.currency]}]")
} catch (e: CryptoException) {
if (logger.isWarnEnabled) logger.warn("$debugMessage => ${e.statusCode}", e)
e.message?.let {
event.respond(it)
}
} catch (e: IOException) {
if (logger.isErrorEnabled) logger.error(debugMessage, e)
event.respond("An IO error has occurred while retrieving the cryptocurrency market price.")
}
} else {
helpResponse(event)
}
}
companion object { companion object {
// Crypto command // Crypto command
private const val CRYPTO_CMD = "crypto" private const val CRYPTO_CMD = "crypto"
@ -101,8 +60,11 @@ class CryptoPrices : AbstractModule() {
// Currency codes keyword // Currency codes keyword
private const val CODES_KEYWORD = "codes" private const val CODES_KEYWORD = "codes"
// Default error message
const val DEFAULT_ERROR_MESSAGE = "An error has occurred while retrieving the cryptocurrency market price"
/** /**
* Get current market price. * Get the current market price.
*/ */
@JvmStatic @JvmStatic
fun currentPrice(args: List<String>): CryptoPrice { fun currentPrice(args: List<String>): CryptoPrice {
@ -156,4 +118,47 @@ class CryptoPrices : AbstractModule() {
} }
loadCurrencies() loadCurrencies()
} }
/**
* Returns the cryptocurrency market price from
* [Coinbase](https://docs.cdp.coinbase.com/coinbase-app/docs/api-prices#get-spot-price).
*/
override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
if (CURRENCIES.isEmpty()) {
try {
loadCurrencies()
} catch (e: ModuleException) {
if (logger.isWarnEnabled) logger.warn(e.debugMessage, e)
}
}
val debugMessage = "crypto($cmd $args)"
if (args == CODES_KEYWORD) {
event.sendMessage("The supported currencies are:")
event.sendList(ArrayList(CURRENCIES.keys), 10, isIndent = true)
} else if (args.matches("\\w+( [a-zA-Z]{3}+)?".toRegex())) {
try {
val price = currentPrice(args.split(' '))
val amount = try {
price.toCurrency()
} catch (_: IllegalArgumentException) {
price.amount
}
event.respond("${price.base} current price is $amount [${CURRENCIES[price.currency]}]")
} catch (e: CryptoException) {
if (logger.isWarnEnabled) logger.warn("$debugMessage => ${e.statusCode}", e)
if (e.message != null) {
event.respond("$DEFAULT_ERROR_MESSAGE: ${e.message}")
} else {
event.respond("$DEFAULT_ERROR_MESSAGE.")
}
} catch (e: IOException) {
if (logger.isErrorEnabled) logger.error(debugMessage, e)
event.respond("$DEFAULT_ERROR_MESSAGE: ${e.message}")
}
} else {
helpResponse(event)
}
}
} }

View file

@ -48,80 +48,12 @@ import java.net.URL
import java.text.DecimalFormat import java.text.DecimalFormat
import java.util.* import java.util.*
/** /**
* The CurrencyConverter module. * Converts between currencies.
*/ */
class CurrencyConverter : AbstractModule() { class CurrencyConverter : AbstractModule() {
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.
@ -134,14 +66,23 @@ class CurrencyConverter : AbstractModule() {
// Currency codes keyword // Currency codes keyword
private const val CODES_KEYWORD = "codes" private const val CODES_KEYWORD = "codes"
// Decimal format
private val DECIMAL_FORMAT = DecimalFormat("0.00#")
// Empty symbols table. // Empty symbols table.
private const val EMPTY_SYMBOLS_TABLE = "Sorry, but the currency table is empty." private const val EMPTY_SYMBOLS_TABLE = "Sorry, but the currency table is empty."
// Logger
private val LOGGER: Logger = LoggerFactory.getLogger(CurrencyConverter::class.java)
// Currency symbols // Currency symbols
private val SYMBOLS: TreeMap<String, String> = TreeMap() private val SYMBOLS: TreeMap<String, String> = TreeMap()
// Decimal format /**
private val DECIMAL_FORMAT = DecimalFormat("0.00#") * No API key error message.
*/
const val ERROR_MESSAGE_NO_API_KEY = "No Exchange Rate API key specified."
/** /**
* Converts from a currency to another. * Converts from a currency to another.
@ -149,7 +90,7 @@ class CurrencyConverter : AbstractModule() {
@JvmStatic @JvmStatic
fun convertCurrency(apiKey: String?, query: String): Message { fun convertCurrency(apiKey: String?, query: String): Message {
if (apiKey.isNullOrEmpty()) { if (apiKey.isNullOrEmpty()) {
throw ModuleException("${CURRENCY_CMD}($query)", "No Exchange Rate API key specified.") throw ModuleException("${CURRENCY_CMD}($query)", ERROR_MESSAGE_NO_API_KEY)
} }
val cmds = query.split(" ") val cmds = query.split(" ")
@ -174,7 +115,10 @@ class CurrencyConverter : AbstractModule() {
} else { } else {
ErrorMessage("Sorry, an error occurred while converting the currencies.") ErrorMessage("Sorry, an error occurred while converting the currencies.")
} }
} catch (ignore: IOException) { } catch (ioe: IOException) {
if (LOGGER.isWarnEnabled) {
LOGGER.warn("IO error while converting currencies: ${ioe.message}", ioe)
}
ErrorMessage("Sorry, an IO error occurred while converting the currencies.") ErrorMessage("Sorry, an IO error occurred while converting the currencies.")
} }
} else { } else {
@ -219,4 +163,74 @@ 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()) -> {
try {
val msg = convertCurrency(properties[API_KEY_PROP], args)
if (msg.isError) {
helpResponse(event)
} else {
event.respond(msg.msg)
}
} catch (e: ModuleException) {
if (LOGGER.isWarnEnabled) LOGGER.warn(e.debugMessage, e)
event.respond(e.message)
}
}
args.contains(CODES_KEYWORD) -> {
event.sendMessage("The supported currency codes are:")
event.sendList(SYMBOLS.keys.toList(), 11, isIndent = true)
}
else -> {
helpResponse(event)
}
}
}
override fun helpResponse(event: GenericMessageEvent): Boolean {
reload(properties[API_KEY_PROP])
if (SYMBOLS.isEmpty()) {
event.sendMessage(EMPTY_SYMBOLS_TABLE)
} else {
val nick = event.bot().nick
event.sendMessage("To convert from one currency to another:")
event.sendMessage(helpFormat(helpCmdSyntax("%c $CURRENCY_CMD 100 USD to EUR", nick, isPrivateMsgEnabled)))
event.sendMessage(
helpFormat(
helpCmdSyntax("%c $CURRENCY_CMD 50,000 GBP to USD", nick, isPrivateMsgEnabled)
)
)
event.sendMessage("To list the supported currency codes:")
event.sendMessage(
helpFormat(
helpCmdSyntax("%c $CURRENCY_CMD $CODES_KEYWORD", nick, isPrivateMsgEnabled)
)
)
}
return true
}
} }

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

@ -31,7 +31,6 @@
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.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,50 +50,29 @@ 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 = SERVICE_NAME
/**
* 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 /**
* API Key property
*/
const val API_KEY_PROP = "google-api-key" const val API_KEY_PROP = "google-api-key"
// Google Custom Search Engine ID property /**
* Google Custom Search Engine ID property
*/
const val CSE_KEY_PROP = "google-cse-cx" const val CSE_KEY_PROP = "google-cse-cx"
/**
* The service name
*/
const val SERVICE_NAME = "GoogleSearch"
// Google command // Google command
private const val GOOGLE_CMD = "google" private const val GOOGLE_CMD = "google"
@ -112,7 +90,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." "$SERVICE_NAME is disabled. The API keys are missing."
) )
} }
val results = mutableListOf<Message>() val results = mutableListOf<Message>()
@ -159,4 +137,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,20 @@ 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) {
try {
randomJoke().forEach {
event.bot().sendIRC().notice(channel, it.msg.colorize(it.color))
}
} catch (e: ModuleException) {
if (logger.isWarnEnabled) logger.warn(e.debugMessage, e)
e.message?.let {
event.respond(it)
}
}
}
} }

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,6 +59,77 @@ 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())
companion object {
// Property keys
const val ACCESS_TOKEN_PROP = "mastodon-access-token"
const val AUTO_POST_PROP = "mastodon-auto-post"
const val HANDLE_PROP = "mastodon-handle"
const val INSTANCE_PROP = "mastodon-instance"
private const val MASTODON_CMD = "mastodon"
private const val TOOT_CMD = "toot"
/**
* Post on Mastodon.
*/
@JvmStatic
@Throws(ModuleException::class)
fun toot(accessToken: String?, instance: String?, handle: String?, message: String, isDm: Boolean): String {
if (accessToken.isNullOrBlank()) {
throw ModuleException("Missing access token", "The access token is missing.")
} else if (instance.isNullOrBlank()) {
throw ModuleException("Missing instance", "The Mastodon instance is missing.")
} else if (isDm && handle.isNullOrBlank()) {
throw ModuleException("Missing handle", "The Mastodon handle is missing.")
}
val request = HttpRequest.newBuilder()
.uri(URI.create("https://$instance/api/v1/statuses"))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer $accessToken")
.POST(
HttpRequest.BodyPublishers.ofString(
JSONWriter.valueToString(
if (isDm) {
mapOf("status" to "${handle?.prefixIfMissing('@')} $message", "visibility" to "direct")
} else {
mapOf("status" to message)
}
)
)
).build()
try {
val response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString())
if (response.statusCode() == 200) {
return try {
val jsonResponse = JSONObject(response.body())
if (isDm) {
jsonResponse.getString("content")
} else {
"Your message was posted to ${jsonResponse.getString("url")}"
}
} catch (e: JSONException) {
throw ModuleException("mastodonPost($message)", "A JSON error has occurred: ${e.message}", e)
}
} else {
throw IOException("HTTP Status Code: " + response.statusCode())
}
} catch (e: IOException) {
throw ModuleException("mastodonPost($message)", "An IO error has occurred: ${e.message}", e)
} catch (e: InterruptedException) {
throw ModuleException("mastodonPost($message)", "An error has occurred: ${e.message}", e)
}
}
}
init {
commands.add(MASTODON_CMD)
commands.add(TOOT_CMD)
help.add("To toot on Mastodon:")
help.add(Utils.helpFormat("%c $TOOT_CMD <message>"))
properties[AUTO_POST_PROP] = "false"
initProperties(ACCESS_TOKEN_PROP, HANDLE_PROP, INSTANCE_PROP)
}
/** /**
* Formats the entry for posting. * Formats the entry for posting.
*/ */
@ -74,76 +148,11 @@ class Mastodon : SocialModule() {
@Throws(ModuleException::class) @Throws(ModuleException::class)
override fun post(message: String, isDm: Boolean): String { override fun post(message: String, isDm: Boolean): String {
return toot( return toot(
apiKey = properties[ACCESS_TOKEN_PROP], accessToken = properties[ACCESS_TOKEN_PROP],
instance = properties[INSTANCE_PROP], instance = properties[INSTANCE_PROP],
handle = handle, handle = handle,
message = message, message = message,
isDm = isDm isDm = isDm
) )
} }
companion object {
// Property keys
const val ACCESS_TOKEN_PROP = "mastodon-access-token"
const val AUTO_POST_PROP = "mastodon-auto-post"
const val HANDLE_PROP = "mastodon-handle"
const val INSTANCE_PROP = "mastodon-instance"
private const val MASTODON_CMD = "mastodon"
private const val TOOT_CMD = "toot"
/**
* Post on Mastodon.
*/
@JvmStatic
@Throws(ModuleException::class)
fun toot(apiKey: String?, instance: String?, handle: String?, message: String, isDm: Boolean): String {
val request = HttpRequest.newBuilder()
.uri(URI.create("https://$instance/api/v1/statuses"))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer $apiKey")
.POST(
HttpRequest.BodyPublishers.ofString(
JSONWriter.valueToString(
if (isDm) {
mapOf("status" to "${handle?.prefixIfMissing('@')} $message", "visibility" to "direct")
} else {
mapOf("status" to message)
}
)
)
)
.build()
try {
val response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString())
if (response.statusCode() == 200) {
return try {
val jsonResponse = JSONObject(response.body())
if (isDm) {
jsonResponse.getString("content")
} else {
"Your message was posted to ${jsonResponse.getString("url")}"
}
} catch (e: JSONException) {
throw ModuleException("mastodonPost($message)", "A JSON error has occurred: ${e.message}", e)
}
} else {
throw IOException("Status Code: " + response.statusCode())
}
} catch (e: IOException) {
throw ModuleException("mastodonPost($message)", "An IO error has occurred: ${e.message}", e)
} catch (e: InterruptedException) {
throw ModuleException("mastodonPost($message)", "An error has occurred: ${e.message}", e)
}
}
}
init {
commands.add(MASTODON_CMD)
commands.add(TOOT_CMD)
help.add("To toot on Mastodon:")
help.add(Utils.helpFormat("%c $TOOT_CMD <message>"))
properties[AUTO_POST_PROP] = "false"
initProperties(ACCESS_TOKEN_PROP, HANDLE_PROP, INSTANCE_PROP)
}
} }

View file

@ -30,16 +30,13 @@
*/ */
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") @Suppress("unused")
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,23 +94,9 @@ 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 - 1).random()]
when { when {
hand == botHand -> { hand == botHand -> {
event.respond("${hand.name} vs. ${botHand.name} » You ${"tie".bold()}.") event.respond("${hand.name} vs. ${botHand.name} » You ${"tie".bold()}.")

View file

@ -30,7 +30,6 @@
*/ */
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.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,33 +48,12 @@ 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 = SERVICE_NAME
/**
* 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 {
/** /**
@ -88,6 +66,11 @@ class StockQuote : AbstractModule() {
*/ */
const val INVALID_SYMBOL = "Invalid symbol." const val INVALID_SYMBOL = "Invalid symbol."
/**
* The service name.
*/
const val SERVICE_NAME = "StockQuote"
// API URL // API URL
private const val API_URL = "https://www.alphavantage.co/query?function=" private const val API_URL = "https://www.alphavantage.co/query?function="
@ -103,7 +86,7 @@ class StockQuote : AbstractModule() {
if (info.isNotEmpty()) { if (info.isNotEmpty()) {
throw ModuleException(debugMessage, info.unescapeXml()) throw ModuleException(debugMessage, info.unescapeXml())
} }
} catch (ignore: JSONException) { } catch (_: JSONException) {
// Do nothing // Do nothing
} }
try { try {
@ -115,7 +98,7 @@ class StockQuote : AbstractModule() {
if (error.isNotEmpty()) { if (error.isNotEmpty()) {
throw ModuleException(debugMessage, error.unescapeXml()) throw ModuleException(debugMessage, error.unescapeXml())
} }
} catch (ignore: JSONException) { } catch (_: JSONException) {
// Do nothing // Do nothing
} }
json json
@ -132,8 +115,8 @@ class StockQuote : AbstractModule() {
fun getQuote(symbol: String, apiKey: String?): List<Message> { fun getQuote(symbol: String, apiKey: String?): List<Message> {
if (apiKey.isNullOrBlank()) { if (apiKey.isNullOrBlank()) {
throw ModuleException( throw ModuleException(
"${StockQuote::class.java.name} is disabled.", "$SERVICE_NAME is disabled.",
"${STOCK_CMD.capitalise()} is disabled. The API key is missing." "$SERVICE_NAME is disabled. The API key is missing."
) )
} }
val messages = mutableListOf<Message>() val messages = mutableListOf<Message>()
@ -233,4 +216,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,37 +51,12 @@ 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_NAME
/**
* 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 {
/** /**
@ -89,6 +64,11 @@ class Weather2 : AbstractModule() {
*/ */
const val API_KEY_PROP = "owm-api-key" const val API_KEY_PROP = "owm-api-key"
/**
* The service name.
*/
const val WEATHER_NAME = "Weather"
// Weather command // Weather command
private const val WEATHER_CMD = "weather" private const val WEATHER_CMD = "weather"
@ -121,7 +101,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 +161,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 +227,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

@ -42,10 +42,84 @@ import org.slf4j.LoggerFactory
import java.io.IOException import java.io.IOException
import java.net.URL import java.net.URL
/**
* Allows user to query Wolfram Alpha.
*/
class WolframAlpha : AbstractModule() { class WolframAlpha : AbstractModule() {
private val logger: Logger = LoggerFactory.getLogger(WolframAlpha::class.java) private val logger: Logger = LoggerFactory.getLogger(WolframAlpha::class.java)
override val name = "WolframAlpha" override val name = SERVICE_NAME
companion object {
/**
* The Wolfram Alpha AppID property.
*/
const val APPID_KEY_PROP = "wolfram-appid"
/**
* Metric unit
*/
const val METRIC = "metric"
/**
* Imperial unit
*/
const val IMPERIAL = "imperial"
/**
* The service name.
*/
const val SERVICE_NAME = "WolframAlpha"
/**
* The Wolfram units properties
*/
const val UNITS_PROP = "wolfram-units"
// Wolfram command
private const val WOLFRAM_CMD = "wolfram"
// Wolfram Alpha API URL
private const val API_URL = "https://api.wolframalpha.com/v1/spoken?appid="
@JvmStatic
@Throws(ModuleException::class)
fun queryWolfram(query: String, units: String = IMPERIAL, appId: String?): String {
if (!appId.isNullOrEmpty()) {
try {
val urlReader = URL("${API_URL}${appId}&units=${units}&i=" + query.encodeUrl()).reader()
if (urlReader.responseCode.isHttpSuccess()) {
return urlReader.body
} else {
throw ModuleException(
"wolfram($query): ${urlReader.responseCode} : ${urlReader.body} ",
urlReader.body.ifEmpty {
"Looks like $SERVICE_NAME isn't able to answer that. (${urlReader.responseCode})"
}
)
}
} catch (ioe: IOException) {
throw ModuleException(
"wolfram($query): IOE", "An IO Error occurred while querying $SERVICE_NAME.", ioe
)
}
} else {
throw ModuleException("wolfram($query): No API Key", "No $SERVICE_NAME AppID specified.")
}
}
}
init {
commands.add(WOLFRAM_CMD)
with(help) {
add("To get answers from Wolfram Alpha:")
add(Utils.helpFormat("%c $WOLFRAM_CMD <query> [units=(${METRIC}|${IMPERIAL})]"))
add("For example:")
add(Utils.helpFormat("%c $WOLFRAM_CMD days until christmas"))
add(Utils.helpFormat("%c $WOLFRAM_CMD distance earth moon units=metric"))
}
initProperties(APPID_KEY_PROP, UNITS_PROP)
}
private fun getUnits(unit: String?): String { private fun getUnits(unit: String?): String {
return if (unit?.lowercase() == METRIC) { return if (unit?.lowercase() == METRIC) {
@ -55,6 +129,9 @@ class WolframAlpha : AbstractModule() {
} }
} }
/**
* Queries Wolfram Alpha.
*/
override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) { override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
if (args.isNotBlank()) { if (args.isNotBlank()) {
try { try {
@ -80,63 +157,4 @@ class WolframAlpha : AbstractModule() {
helpResponse(event) helpResponse(event)
} }
} }
companion object {
/**
* The Wolfram Alpha API Key property.
*/
const val APPID_KEY_PROP = "wolfram-appid"
/**
* The Wolfram units properties
*/
const val UNITS_PROP = "wolfram-units"
const val METRIC = "metric"
const val IMPERIAL = "imperial"
// Wolfram command
private const val WOLFRAM_CMD = "wolfram"
// Wolfram Alpha API URL
private const val API_URL = "http://api.wolframalpha.com/v1/spoken?appid="
@JvmStatic
@Throws(ModuleException::class)
fun queryWolfram(query: String, units: String = IMPERIAL, appId: String?): String {
if (!appId.isNullOrEmpty()) {
try {
val urlReader = URL("${API_URL}${appId}&units=${units}&i=" + query.encodeUrl()).reader()
if (urlReader.responseCode.isHttpSuccess()) {
return urlReader.body
} else {
throw ModuleException(
"wolfram($query): ${urlReader.responseCode} : ${urlReader.body} ",
urlReader.body.ifEmpty {
"Looks like Wolfram Alpha isn't able to answer that. (${urlReader.responseCode})"
}
)
}
} catch (ioe: IOException) {
throw ModuleException(
"wolfram($query): IOE", "An IO Error occurred while querying Wolfram Alpha.", ioe
)
}
} else {
throw ModuleException("wolfram($query): No API Key", "No Wolfram Alpha API key specified.")
}
}
}
init {
commands.add(WOLFRAM_CMD)
with(help) {
add("To get answers from Wolfram Alpha:")
add(Utils.helpFormat("%c $WOLFRAM_CMD <query> [units=(${METRIC}|${IMPERIAL})]"))
add("For example:")
add(Utils.helpFormat("%c $WOLFRAM_CMD days until christmas"))
add(Utils.helpFormat("%c $WOLFRAM_CMD distance earth moon units=metric"))
}
initProperties(APPID_KEY_PROP, UNITS_PROP)
}
} }

View file

@ -328,7 +328,7 @@ class WorldTime : AbstractModule() {
// The Time command // The Time command
private const val TIME_CMD = "time" private const val TIME_CMD = "time"
// The zones arguments // The `zones` arguments
private const val ZONES_ARGS = "zones" private const val ZONES_ARGS = "zones"
// The default zone // The default zone
@ -367,6 +367,16 @@ class WorldTime : AbstractModule() {
} }
} }
init {
with(help) {
add("To display a country's current date/time:")
add(helpFormat("%c $TIME_CMD [<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,9 @@ 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, 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)
@ -65,7 +68,7 @@ abstract class SocialModule : AbstractModule() {
abstract fun post(message: String, isDm: Boolean): String abstract fun post(message: String, isDm: Boolean): String
/** /**
* Post entry to social media. * Post an entry to social media.
*/ */
fun postEntry(index: Int) { fun postEntry(index: Int) {
if (isAutoPost && LinksManager.entries.links.size >= index) { if (isAutoPost && LinksManager.entries.links.size >= index) {

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,35 +40,26 @@ 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 {
val response = val response =
URL("https://api.pinboard.in/v1/posts/get?auth_token=${apiToken}&tag=test&" + url.encodeUrl()).reader().body URL(
"https://api.pinboard.in/v1/posts/get?auth_token=${apiToken}&tag=test&" + url.encodeUrl()
).reader().body
matches.forEach { matches.forEach {
if (!response.contains(it)) { if (!response.contains(it)) {
@ -78,4 +69,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,224 @@ 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())
} }
}
@Nested
@DisplayName("Properties Tests")
inner class PropertiesTests {
@Test @Test
fun testIsoLocalDate() { fun `Convert properties to int`() {
assertThat(cal.time.toIsoLocalDate(), "isoLocalDate(date)").isEqualTo("1952-02-17") assertThat(p.getIntProperty("one", 9), "getIntProperty(one)").isEqualTo(1)
assertThat(localDateTime.toIsoLocalDate(), "isoLocalDate(localDate)").isEqualTo("1952-02-17") assertThat(p.getIntProperty("two", 2), "getIntProperty(two)").isEqualTo(2)
} }
@Test @Test
fun testLastOrEmpty() { 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 +299,135 @@ 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

@ -37,14 +37,70 @@ import assertk.assertions.isInstanceOf
import net.objecthunter.exp4j.tokenizer.UnknownFunctionOrVariableException import net.objecthunter.exp4j.tokenizer.UnknownFunctionOrVariableException
import net.thauvin.erik.mobibot.Utils.bold import net.thauvin.erik.mobibot.Utils.bold
import net.thauvin.erik.mobibot.modules.Calc.Companion.calculate import net.thauvin.erik.mobibot.modules.Calc.Companion.calculate
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Nested
import org.mockito.Mockito
import org.pircbotx.hooks.types.GenericMessageEvent
import kotlin.test.Test import kotlin.test.Test
class CalcTest { class CalcTest {
@Nested
@DisplayName("Calculate Tests")
inner class CalculateTests {
@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()}")
assertFailure { calculate("one + one") }.isInstanceOf(UnknownFunctionOrVariableException::class.java) }
@Test
fun `Calculate scientific notations`() {
assertThat(calculate("3e2 - 3e1"), "calculate(3e2-3e1 )").isEqualTo("3e2-3e1 = ${"270".bold()}")
}
@Test
fun `Calculate trigonometric functions`() {
assertThat(calculate("3*sin(10)-cos(2)"), "calculate(3*sin(10)-cos(2)")
.isEqualTo("3*sin(10)-cos(2) = ${"-1.22".bold()}")
}
@Test
fun `Empty calculation should throw exception`() {
assertFailure { calculate(" ") }.isInstanceOf(IllegalArgumentException::class.java)
}
@Test
fun `Invalid calculation should throw exception`() {
assertFailure { calculate("a + b = c") }.isInstanceOf(UnknownFunctionOrVariableException::class.java)
}
}
@Nested
@DisplayName("Command Response Tests")
inner class CommandResponseTests {
@Test
fun `Basic calculation`() {
val calc = Calc()
val event = Mockito.mock(GenericMessageEvent::class.java)
calc.commandResponse("channel", "calc", "1 + 1 * 2", event)
Mockito.verify(event, Mockito.times(1)).respond("1+1*2 = ${"3".bold()}")
}
@Test
fun `Invalid calculation`() {
val calc = Calc()
val event = Mockito.mock(GenericMessageEvent::class.java)
calc.commandResponse("channel", "calc", "two + two", event)
Mockito.verify(event, Mockito.times(1)).respond("No idea. This is the kind of math I don't get.")
}
} }
} }

View file

@ -34,28 +34,62 @@ import assertk.assertFailure
import assertk.assertThat import assertk.assertThat
import assertk.assertions.contains import assertk.assertions.contains
import assertk.assertions.hasNoCause import assertk.assertions.hasNoCause
import assertk.assertions.isEqualTo
import assertk.assertions.isInstanceOf import assertk.assertions.isInstanceOf
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 org.mockito.ArgumentCaptor
import org.mockito.Mockito
import org.pircbotx.hooks.types.GenericMessageEvent
import kotlin.test.Test import kotlin.test.Test
class ChatGpt2Test : LocalProperties() { class ChatGpt2Test : LocalProperties() {
@Nested
@DisplayName("Command Response Tests")
inner class CommandResponseTests {
@Test @Test
fun testApiKey() { fun moduleMisconfigured() {
val chatGpt2 = ChatGpt2()
val event = Mockito.mock(GenericMessageEvent::class.java)
val captor = ArgumentCaptor.forClass(String::class.java)
chatGpt2.commandResponse("channel", "chatgpt", "1 liter to gallon", event)
Mockito.verify(event, Mockito.atLeastOnce()).respond(captor.capture())
assertThat(captor.value).isEqualTo("The ${ChatGpt2.CHATGPT_NAME} module is misconfigured.")
}
}
@Nested
@DisplayName("Chat Tests")
inner class ChatTests {
@Test
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()
} }
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") "javascript function to make a request with XMLHttpRequest, just code",
apiKey,
50
)
).contains("```javascript")
}
@Test
@DisableOnCi
fun chatFailure() {
assertFailure { ChatGpt2.chat("1 liter to gallon", apiKey, -1) } assertFailure { ChatGpt2.chat("1 liter to gallon", apiKey, -1) }
.isInstanceOf(ModuleException::class.java) .isInstanceOf(ModuleException::class.java)
} }
} }
}

View file

@ -32,47 +32,22 @@ package net.thauvin.erik.mobibot.modules
import assertk.all import assertk.all
import assertk.assertThat import assertk.assertThat
import assertk.assertions.isEqualTo import assertk.assertions.*
import assertk.assertions.isGreaterThan
import assertk.assertions.prop
import net.thauvin.erik.crypto.CryptoPrice import net.thauvin.erik.crypto.CryptoPrice
import net.thauvin.erik.mobibot.modules.CryptoPrices.Companion.currentPrice 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 org.mockito.ArgumentCaptor
import org.mockito.Mockito
import org.pircbotx.hooks.types.GenericMessageEvent
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
class CryptoPricesTest { class CryptoPricesTest {
init {
loadCurrencies()
}
@Test
@Throws(ModuleException::class)
fun testMarketPrice() {
var price = currentPrice(listOf("BTC"))
assertThat(price, "currentPrice(BTC)").all {
prop(CryptoPrice::base).isEqualTo("BTC")
prop(CryptoPrice::currency).isEqualTo("USD")
prop(CryptoPrice::amount).transform { it.signum() }.isGreaterThan(0)
}
price = currentPrice(listOf("ETH", "EUR"))
assertThat(price, "currentPrice(ETH, EUR)").all {
prop(CryptoPrice::base).isEqualTo("ETH")
prop(CryptoPrice::currency).isEqualTo("EUR")
prop(CryptoPrice::amount).transform { it.signum() }.isGreaterThan(0)
}
}
@Test
fun testGetCurrencyName() {
assertThat(getCurrencyName("USD"), "USD").isEqualTo("United States Dollar")
assertThat(getCurrencyName("EUR"), "EUR").isEqualTo("Euro")
}
companion object { companion object {
@JvmStatic @JvmStatic
@BeforeAll @BeforeAll
@ -84,4 +59,89 @@ class CryptoPricesTest {
} }
} }
} }
init {
loadCurrencies()
}
@Nested
@DisplayName("Command Response Tests")
inner class CommandResponseTests {
@Test
fun `Current price for BTC`() {
val cryptoPrices = CryptoPrices()
val event = Mockito.mock(GenericMessageEvent::class.java)
val captor = ArgumentCaptor.forClass(String::class.java)
cryptoPrices.commandResponse("channel", "crypto", "btc", event)
Mockito.verify(event, Mockito.times(1)).respond(captor.capture())
assertThat(captor.value).startsWith("BTC current price is $")
}
@Test
fun `Current price for BTC in EUR`() {
val cryptoPrices = CryptoPrices()
val event = Mockito.mock(GenericMessageEvent::class.java)
val captor = ArgumentCaptor.forClass(String::class.java)
cryptoPrices.commandResponse("channel", "crypto", "eth eur", event)
Mockito.verify(event, Mockito.times(1)).respond(captor.capture())
assertThat(captor.value).matches(Regex("ETH current price is €.* \\[Euro]"))
}
@Test
fun `Invalid crypto symbol`() {
val cryptoPrices = CryptoPrices()
val event = Mockito.mock(GenericMessageEvent::class.java)
val captor = ArgumentCaptor.forClass(String::class.java)
cryptoPrices.commandResponse("channel", "crypto", "foo", event)
Mockito.verify(event, Mockito.times(1)).respond(captor.capture())
assertThat(captor.value)
.isEqualTo("${CryptoPrices.DEFAULT_ERROR_MESSAGE}: not found")
}
}
@Nested
@DisplayName("Currency Name Tests")
inner class CurrencyNameTests {
@Test
fun `Currency name for USD`() {
assertThat(getCurrencyName("USD"), "USD").isEqualTo("United States Dollar")
}
@Test
fun `Currency name for EUR`() {
assertThat(getCurrencyName("EUR"), "EUR").isEqualTo("Euro")
}
}
@Nested
@DisplayName("Current Price Tests")
inner class CurrentPriceTests {
@Test
@Throws(ModuleException::class)
fun `Current price for Bitcoin`() {
val price = currentPrice(listOf("BTC"))
assertThat(price, "currentPrice(BTC)").all {
prop(CryptoPrice::base).isEqualTo("BTC")
prop(CryptoPrice::currency).isEqualTo("USD")
prop(CryptoPrice::amount).transform { it.signum() }.isGreaterThan(0)
}
}
@Test
@Throws(ModuleException::class)
fun `Current price for Ethereum in Euro`() {
val price = currentPrice(listOf("ETH", "EUR"))
assertThat(price, "currentPrice(ETH, EUR)").all {
prop(CryptoPrice::base).isEqualTo("ETH")
prop(CryptoPrice::currency).isEqualTo("EUR")
prop(CryptoPrice::amount).transform { it.signum() }.isGreaterThan(0)
}
}
}
} }

View file

@ -32,16 +32,18 @@ package net.thauvin.erik.mobibot.modules
import assertk.all import assertk.all
import assertk.assertThat import assertk.assertThat
import assertk.assertions.contains import assertk.assertions.*
import assertk.assertions.isInstanceOf
import assertk.assertions.matches
import assertk.assertions.prop
import net.thauvin.erik.mobibot.LocalProperties import net.thauvin.erik.mobibot.LocalProperties
import net.thauvin.erik.mobibot.modules.CurrencyConverter.Companion.convertCurrency import net.thauvin.erik.mobibot.modules.CurrencyConverter.Companion.convertCurrency
import net.thauvin.erik.mobibot.modules.CurrencyConverter.Companion.loadSymbols 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 org.mockito.ArgumentCaptor
import org.mockito.Mockito
import org.pircbotx.hooks.types.GenericMessageEvent
import kotlin.test.Test import kotlin.test.Test
class CurrencyConverterTest : LocalProperties() { class CurrencyConverterTest : LocalProperties() {
@ -50,28 +52,80 @@ class CurrencyConverterTest : LocalProperties() {
loadSymbols(apiKey) loadSymbols(apiKey)
} }
@Nested
@DisplayName("Command Response Tests")
inner class CommandResponseTests {
@Test @Test
fun testConvertCurrency() { fun `USD to CAD`() {
val apiKey = getProperty(CurrencyConverter.API_KEY_PROP) val currencyConverter = CurrencyConverter()
assertThat( val event = Mockito.mock(GenericMessageEvent::class.java)
convertCurrency(apiKey, "100 USD to EUR").msg, val captor = ArgumentCaptor.forClass(String::class.java)
"convertCurrency(100 USD to EUR)"
).matches("100 United States Dollar = \\d{2,3}\\.\\d{2,3} Euro".toRegex()) currencyConverter.properties.put(
assertThat( CurrencyConverter.API_KEY_PROP, getProperty(CurrencyConverter.API_KEY_PROP)
convertCurrency(apiKey, "1 USD to GBP").msg, )
"convertCurrency(1 USD to BGP)" currencyConverter.commandResponse("channel", "currency", "1 USD to CAD", event)
).matches("1 United States Dollar = 0\\.\\d{2,3} Pound Sterling".toRegex())
Mockito.verify(event, Mockito.atLeastOnce()).respond(captor.capture())
assertThat(captor.value).matches("1 United States Dollar = \\d+\\.\\d{2,3} Canadian Dollar".toRegex())
}
@Test
fun `API Key is not specified`() {
val currencyConverter = CurrencyConverter()
val event = Mockito.mock(GenericMessageEvent::class.java)
val captor = ArgumentCaptor.forClass(String::class.java)
currencyConverter.commandResponse("channel", "currency", "1 USD to CAD", event)
Mockito.verify(event, Mockito.atLeastOnce()).respond(captor.capture())
assertThat(captor.value).isEqualTo(CurrencyConverter.ERROR_MESSAGE_NO_API_KEY)
}
}
@Nested
@DisplayName("Currency Converter Tests")
inner class CurrencyConverterTests {
private val apiKey = getProperty(CurrencyConverter.API_KEY_PROP)
@Test
fun `Convert CAD to USD`() {
assertThat( 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,109 @@ 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 assertk.assertions.startsWith
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.RepeatedTest
import org.mockito.ArgumentCaptor
import org.mockito.Mockito
import org.pircbotx.hooks.types.GenericMessageEvent
import kotlin.random.Random
import kotlin.test.Test import kotlin.test.Test
class DiceTest { class DiceTest {
@Nested
@DisplayName("Command Response Tests")
inner class CommandResponseTests {
@Test @Test
fun testRoll() { fun `Roll die`() {
assertThat(Dice.roll(1, 1), "roll(1d1)").isEqualTo("\u00021\u0002") val dice = Dice()
assertThat(Dice.roll(2, 1), "roll(2d1)") val event = Mockito.mock(GenericMessageEvent::class.java)
.isEqualTo("\u00021\u0002 + \u00021\u0002 = \u00022\u0002") val captor = ArgumentCaptor.forClass(String::class.java)
assertThat(Dice.roll(5, 1), "roll(5d1)")
.isEqualTo("\u00021\u0002 + \u00021\u0002 + \u00021\u0002 + \u00021\u0002 + \u00021\u0002 = \u00025\u0002") dice.commandResponse("channel", "dice", "", event)
assertThat(Dice.roll(2, 6), "roll(2d6)")
Mockito.verify(event, Mockito.times(1)).respond(captor.capture())
assertThat(captor.value).startsWith("you rolled")
}
@RepeatedTest(3)
fun `Roll die with 9 sides`() {
val dice = Dice()
val event = Mockito.mock(GenericMessageEvent::class.java)
val captor = ArgumentCaptor.forClass(String::class.java)
dice.commandResponse("channel", "dice", "1d9", event)
Mockito.verify(event, Mockito.times(1)).respond(captor.capture())
assertThat(captor.value).matches("you rolled \u0002[1-9]\u0002".toRegex())
}
@RepeatedTest(3)
fun `Roll dice`() {
val dice = Dice()
val event = Mockito.mock(GenericMessageEvent::class.java)
val captor = ArgumentCaptor.forClass(String::class.java)
dice.commandResponse("channel", "dice", "2d6", event)
Mockito.verify(event, Mockito.times(1)).respond(captor.capture())
assertThat(captor.value)
.matches("you rolled \u0002[1-6]\u0002 \\+ \u0002[1-6]\u0002 = \u0002\\d{1,2}\\u0002".toRegex())
}
}
@Nested
@DisplayName("Roll Tests")
inner class RollTests {
@Test
fun `Roll die`() {
assertThat(Dice.roll(1, 6)).matches("\u0002[1-6]\u0002".toRegex())
}
@Test
fun `Roll die with 1 side`() {
assertThat(Dice.roll(1, 1)).isEqualTo("\u00021\u0002")
}
@RepeatedTest(5)
fun `Roll die with random sides`() {
assertThat(Dice.roll(1, Random.nextInt(1, 11))).matches("\u0002([1-9]|10)\u0002".toRegex())
}
@Test
fun `Roll 2 dice`() {
assertThat(Dice.roll(2, 6))
.matches("\u0002[1-6]\u0002 \\+ \u0002[1-6]\u0002 = \u0002[1-9][0-2]?\u0002".toRegex()) .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 2 dice with 1 side`() {
assertThat(Dice.roll(2, 1)).isEqualTo("\u00021\u0002 + \u00021\u0002 = \u00022\u0002")
}
@Test
fun `Roll 3 dice with 1 side`() {
assertThat(Dice.roll(4, 1))
.isEqualTo("\u00021\u0002 + \u00021\u0002 + \u00021\u0002 + \u00021\u0002 = \u00024\u0002")
}
@Test
fun `Roll 3 dice with 7 sides`() {
assertThat(Dice.roll(3, 7))
.matches(
"\u0002[1-7]\u0002 \\+ \u0002[1-7]\u0002 \\+ \u0002[1-7]\u0002 = \u0002\\d{1,2}\u0002"
.toRegex()
)
}
@RepeatedTest(3)
fun `Roll 3 dice with random sides`() {
assertThat(Dice.roll(3, Random.nextInt(1, 6)))
.matches(
"\u0002[1-5]\u0002 \\+ \u0002[1-5]\u0002 \\+ \u0002[1-5]\u0002 = \u0002\\d{1,2}\u0002"
.toRegex()
)
}
} }
} }

View file

@ -35,31 +35,72 @@ 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 org.mockito.ArgumentCaptor
import org.mockito.Mockito
import org.pircbotx.hooks.types.GenericMessageEvent
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(
"javascript function to make a request with XMLHttpRequest, just code",
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( assertThat(
Gemini2.chat("how do I make an HTTP request in Javascript?", apiKey, maxTokens) Gemini2.chat("encode a url in java, one line, just code", apiKey, 60)
).isNotNull().contains("XMLHttpRequest") ).isNotNull().contains("UrlEncoder", true)
}
}
assertThat( @Nested
Gemini2.chat("how do I encode a URL in java?", apiKey, 60) @DisplayName("Command Response Tests")
).isNotNull().contains("URLEncoder") inner class CommandResponseTests {
@Test
fun moduleMisconfigured() {
val gemini2 = Gemini2()
val event = Mockito.mock(GenericMessageEvent::class.java)
val captor = ArgumentCaptor.forClass(String::class.java)
gemini2.commandResponse("channel", "gemini", "1 liter to gallon", event)
Mockito.verify(event, Mockito.atLeastOnce()).respond(captor.capture())
assertThat(captor.value).isEqualTo("The ${Gemini2.GEMINI_NAME} module is misconfigured.")
}
}
@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,22 @@ 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 org.mockito.ArgumentCaptor
import org.mockito.Mockito
import org.mockito.kotlin.whenever
import org.pircbotx.hooks.types.GenericMessageEvent
import kotlin.test.Test import kotlin.test.Test
class GoogleSearchTest : LocalProperties() { class GoogleSearchTest : LocalProperties() {
@Test private val apiKey = getProperty(GoogleSearch.API_KEY_PROP)
fun testAPIKeys() { private val cseKey = getProperty(GoogleSearch.CSE_KEY_PROP)
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 +65,101 @@ class GoogleSearchTest : LocalProperties() {
} }
} }
} }
@Nested
@DisplayName("Command Response Tests")
inner class CommandResponseTests {
@Test
fun `API Keys are missing`() {
val googleSearch = GoogleSearch()
val event = Mockito.mock(GenericMessageEvent::class.java)
val captor = ArgumentCaptor.forClass(String::class.java)
val user = Mockito.mock(org.pircbotx.User::class.java)
whenever(event.user).thenReturn(user)
whenever(user.nick).thenReturn("mock")
googleSearch.commandResponse("channel", "google", "seattle seahawks", event)
Mockito.verify(event, Mockito.atLeastOnce()).respond(captor.capture())
assertThat(captor.value)
.isEqualTo("${GoogleSearch.SERVICE_NAME} is disabled. The API keys are missing.")
}
@Test
fun `No results found`() {
val googleSearch = GoogleSearch()
val event = Mockito.mock(GenericMessageEvent::class.java)
val captor = ArgumentCaptor.forClass(String::class.java)
val user = Mockito.mock(org.pircbotx.User::class.java)
whenever(event.user).thenReturn(user)
whenever(user.nick).thenReturn("mock")
googleSearch.properties.put(GoogleSearch.API_KEY_PROP, apiKey)
googleSearch.properties.put(GoogleSearch.CSE_KEY_PROP, cseKey)
googleSearch.commandResponse("channel", "google", "\"foobarbarfoofoobarblahblah\"", event)
Mockito.verify(event, Mockito.atLeastOnce()).respond(captor.capture())
assertThat(captor.value)
.isEqualTo("\u000304No results found.\u000F")
}
}
@Nested
@DisplayName("API Keys Test")
inner class ApiKeysTest {
@Test
fun `API key should not be empty`() {
assertFailure { sanitizedSearch("test", "", "apiKey") }
.isInstanceOf(ModuleException::class.java).hasNoCause()
}
@Test
fun `CSE key should not empty`() {
assertFailure { sanitizedSearch("test", "apiKey", "") }
.isInstanceOf(ModuleException::class.java).hasNoCause()
}
@Test
fun `Invalid API key should throw exception`() {
assertFailure { sanitizedSearch("test", "apiKey", "cssKey") }
.isInstanceOf(ModuleException::class.java)
.hasMessage("API key not valid. Please pass a valid API key.")
}
}
@Nested
@DisplayName("Search Tests")
inner class SearchTests {
@Test
fun `Query should not be empty`() {
assertThat(sanitizedSearch("", apiKey, cseKey).first()).isInstanceOf(ErrorMessage::class.java)
}
@Test
@DisableOnCi
@Throws(ModuleException::class)
fun `No results found`() {
val query = "adadflkjl"
val messages = sanitizedSearch(query, apiKey, cseKey)
assertThat(messages, "searchGoogle($query)").index(0).all {
isInstanceOf(ErrorMessage::class.java)
prop(Message::msg).isEqualTo("No results found.")
}
}
@Test
@DisableOnCi
@Throws(ModuleException::class)
fun `Search Google`() {
val query = "mobibot"
val messages = sanitizedSearch(query, apiKey, cseKey)
assertThat(messages, "searchGoogle($query)").all {
isNotEmpty()
index(0).prop(Message::msg).contains(query, true)
}
}
}
} }

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

@ -33,24 +33,80 @@ package net.thauvin.erik.mobibot.modules
import assertk.assertThat import assertk.assertThat
import assertk.assertions.any import assertk.assertions.any
import assertk.assertions.contains import assertk.assertions.contains
import assertk.assertions.isEqualTo
import net.thauvin.erik.mobibot.modules.Lookup.Companion.nslookup import net.thauvin.erik.mobibot.modules.Lookup.Companion.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 org.mockito.ArgumentCaptor
import org.mockito.Mockito
import org.pircbotx.hooks.types.GenericMessageEvent
import kotlin.test.Test import kotlin.test.Test
class LookupTest { class LookupTest {
@Nested
@DisplayName("Command Response Tests")
inner class CommandResponseTests {
@Test
fun lookupByHostname() {
val lookup = Lookup()
val event = Mockito.mock(GenericMessageEvent::class.java)
val captor = ArgumentCaptor.forClass(String::class.java)
lookup.commandResponse("channel", "lookup", "ec2-54-234-237-183.compute-1.amazonaws.com", event)
Mockito.verify(event, Mockito.times(1)).respondWith(captor.capture())
assertThat(captor.value).contains("54.234.237.183")
}
@Test
fun lookupByAddress() {
val lookup = Lookup()
val event = Mockito.mock(GenericMessageEvent::class.java)
val captor = ArgumentCaptor.forClass(String::class.java)
lookup.commandResponse("channel", "lookup", "54.234.237.183", event)
Mockito.verify(event, Mockito.times(1)).respondWith(captor.capture())
assertThat(captor.value).contains("ec2-54-234-237-183.compute-1.amazonaws.com")
}
@Test
fun lookupUnknownHostname() {
val lookup = Lookup()
val event = Mockito.mock(GenericMessageEvent::class.java)
val captor = ArgumentCaptor.forClass(String::class.java)
lookup.commandResponse("channel", "lookup", "foobar", event)
Mockito.verify(event, Mockito.times(1)).respond(captor.capture())
assertThat(captor.value).isEqualTo("Unknown host.")
}
}
@Nested
@DisplayName("Lookup Tests")
inner class LookupTests {
@Test @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

@ -30,16 +30,94 @@
*/ */
package net.thauvin.erik.mobibot.modules package net.thauvin.erik.mobibot.modules
import assertk.assertFailure
import assertk.assertThat import assertk.assertThat
import assertk.assertions.contains import assertk.assertions.contains
import assertk.assertions.hasMessage
import assertk.assertions.isEqualTo
import assertk.assertions.isInstanceOf
import net.thauvin.erik.mobibot.LocalProperties import net.thauvin.erik.mobibot.LocalProperties
import net.thauvin.erik.mobibot.modules.Mastodon.Companion.toot import net.thauvin.erik.mobibot.modules.Mastodon.Companion.toot
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Nested
import org.mockito.ArgumentCaptor
import org.mockito.Mockito
import org.mockito.kotlin.whenever
import org.pircbotx.hooks.types.GenericMessageEvent
import kotlin.test.Test import kotlin.test.Test
class MastodonTest : LocalProperties() { class MastodonTest : LocalProperties() {
@Nested
@DisplayName("Command Response Tests")
inner class CommandResponseTests {
@Test
fun `API Key is not specified`() {
val mastodon = Mastodon()
val event = Mockito.mock(GenericMessageEvent::class.java)
val captor = ArgumentCaptor.forClass(String::class.java)
val user = Mockito.mock(org.pircbotx.User::class.java)
whenever(event.user).thenReturn(user)
whenever(user.nick).thenReturn("mock")
mastodon.commandResponse("channel", "toot", "This is a test.", event)
Mockito.verify(event, Mockito.atLeastOnce()).respond(captor.capture())
assertThat(captor.value).isEqualTo("The access token is missing.")
}
}
@Nested
@DisplayName("Toot Tests")
inner class TootTests {
@Test @Test
@Throws(ModuleException::class) @Throws(ModuleException::class)
fun testToot() { fun `Empty Access Token should throw exception`() {
val msg = "Testing Mastodon API from ${getHostName()}"
assertFailure {
toot(
"",
getProperty(Mastodon.INSTANCE_PROP),
getProperty(Mastodon.HANDLE_PROP),
msg,
true
)
}.isInstanceOf(ModuleException::class.java).hasMessage("The access token is missing.")
}
@Test
@Throws(ModuleException::class)
fun `Empty Handle should throw exception`() {
val msg = "Testing Mastodon API from ${getHostName()}"
assertFailure {
toot(
getProperty(Mastodon.ACCESS_TOKEN_PROP),
getProperty(Mastodon.INSTANCE_PROP),
"",
msg,
true
)
}.isInstanceOf(ModuleException::class.java).hasMessage("The Mastodon handle is missing.")
}
@Test
@Throws(ModuleException::class)
fun `Empty Instance should throw exception`() {
val msg = "Testing Mastodon API from ${getHostName()}"
assertFailure {
toot(
getProperty(Mastodon.ACCESS_TOKEN_PROP),
"",
getProperty(Mastodon.HANDLE_PROP),
msg,
true
)
}.isInstanceOf(ModuleException::class.java).hasMessage("The Mastodon instance is missing.")
}
@Test
@Throws(ModuleException::class)
fun `Toot on Mastodon`() {
val msg = "Testing Mastodon API from ${getHostName()}" val msg = "Testing Mastodon API from ${getHostName()}"
assertThat( assertThat(
toot( toot(
@ -52,3 +130,4 @@ class MastodonTest : LocalProperties() {
).contains(msg) ).contains(msg)
} }
} }
}

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,31 +47,34 @@ 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 https://foo.com?apiKey=$apiKey&userID=me"))
assertThat( assertThat(
e.sanitize(apiKey, "", "me").message, "ModuleException(debugMessage, message, IOException(url))" e.sanitize(apiKey, "", "me").message, "ModuleException(debugMessage, message, IOException(url))"
).isNotNull().all { ).isNotNull().all {
@ -102,3 +107,4 @@ 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 random 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

@ -33,18 +33,82 @@ 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 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 org.junit.jupiter.api.RepeatedTest
import org.mockito.ArgumentCaptor
import org.mockito.Mockito
import org.pircbotx.hooks.types.GenericMessageEvent
import kotlin.test.Test import kotlin.test.Test
class RockPaperScissorsTest { class RockPaperScissorsTest {
@Nested
@DisplayName("Command Response Tests")
inner class CommandResponseTests {
@RepeatedTest(3)
fun `Play Rock Paper Scissors`() {
val rockPaperScissors = RockPaperScissors()
val event = Mockito.mock(GenericMessageEvent::class.java)
val captor = ArgumentCaptor.forClass(String::class.java)
rockPaperScissors.commandResponse("channel", "rock", "", event)
Mockito.verify(event, Mockito.atLeastOnce()).respond(captor.capture())
assertThat(captor.value)
.matches(
".* (vs\\.|crushes|covers|cuts) (ROCK|PAPER|SCISSORS) » You \u0002(tie|win|lose)\u0002.".toRegex()
)
}
}
@Nested
@DisplayName("Win, Lose or Draw Tests")
inner class WinLoseOrDrawTests {
@Test @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