Moved to PircBotX and assertk.

This commit is contained in:
Erik C. Thauvin 2021-11-08 13:54:48 -08:00
parent 2a46761dc5
commit 9fb870648e
83 changed files with 2347 additions and 2577 deletions

View file

@ -31,5 +31,10 @@
<option name="name" value="MavenLocal" /> <option name="name" value="MavenLocal" />
<option name="url" value="file:$MAVEN_REPOSITORY$/" /> <option name="url" value="file:$MAVEN_REPOSITORY$/" />
</remote-repository> </remote-repository>
<remote-repository>
<option name="id" value="maven" />
<option name="name" value="maven" />
<option name="url" value="https://jitpack.io" />
</remote-repository>
</component> </component>
</project> </project>

7
.idea/mobibot.iml generated Normal file
View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<module version="4">
<component name="SonarLintModuleSettings">
<option name="idePathPrefix" value="bin/main" />
<option name="sqPathPrefix" value="src/main/kotlin" />
</component>
</module>

View file

@ -2,12 +2,12 @@ plugins {
id 'application' id 'application'
id 'com.github.ben-manes.versions' version '0.39.0' id 'com.github.ben-manes.versions' version '0.39.0'
id 'idea' id 'idea'
id 'io.gitlab.arturbosch.detekt' version '1.18.1' id 'io.gitlab.arturbosch.detekt' version '1.19.0-RC1'
id 'jacoco' id 'jacoco'
id 'java' id 'java'
id 'net.thauvin.erik.gradle.semver' version '1.0.4' id 'net.thauvin.erik.gradle.semver' version '1.0.4'
id 'org.jetbrains.kotlin.jvm' version '1.6.0-RC' id 'org.jetbrains.kotlin.jvm' version '1.6.0-RC2'
id 'org.jetbrains.kotlin.kapt' version '1.6.0-RC' id 'org.jetbrains.kotlin.kapt' version '1.6.0-RC2'
id 'org.sonarqube' version '3.3' id 'org.sonarqube' version '3.3'
id 'pmd' id 'pmd'
} }
@ -22,12 +22,13 @@ mainClassName = packageName + '.Mobibot'
ext.versions = [ ext.versions = [
log4j: '2.14.1', log4j: '2.14.1',
pmd : '6.35.0', pmd : '6.40.0',
] ]
repositories { repositories {
mavenLocal() mavenLocal()
mavenCentral() mavenCentral()
maven { url 'https://jitpack.io' }
maven { url 'https://oss.sonatype.org/content/repositories/snapshots' } maven { url 'https://oss.sonatype.org/content/repositories/snapshots' }
} }
@ -35,8 +36,13 @@ dependencies {
kapt(semverProcessor) kapt(semverProcessor)
compileOnly(semverProcessor) compileOnly(semverProcessor)
implementation 'pircbot:pircbot:1.5.0' implementation 'com.github.pircbotx:pircbotx:-SNAPSHOT'
compileOnly 'pircbot:pircbot:1.5.0:sources'
implementation 'org.apache.commons:commons-text:1.9'
implementation 'org.apache.commons:commons-lang3:3.12.0'
implementation 'org.slf4j:slf4j-api:1.7.32'
implementation 'commons-codec:commons-codec:1.15'
implementation 'com.google.guava:guava:31.0.1-jre'
implementation(platform("org.jetbrains.kotlin:kotlin-bom")) implementation(platform("org.jetbrains.kotlin:kotlin-bom"))
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2'
@ -44,11 +50,12 @@ dependencies {
implementation "org.apache.logging.log4j:log4j-api:$versions.log4j" implementation "org.apache.logging.log4j:log4j-api:$versions.log4j"
implementation "org.apache.logging.log4j:log4j-core:$versions.log4j" implementation "org.apache.logging.log4j:log4j-core:$versions.log4j"
implementation "org.apache.logging.log4j:log4j-slf4j-impl:$versions.log4j" implementation "org.apache.logging.log4j:log4j-slf4j-impl:$versions.log4j"
implementation 'commons-cli:commons-cli:1.4'
implementation 'commons-cli:commons-cli:1.5.0'
implementation 'commons-net:commons-net:3.8.0' implementation 'commons-net:commons-net:3.8.0'
implementation 'com.rometools:rome:1.16.0' implementation 'com.rometools:rome:1.16.0'
implementation 'com.squareup.okhttp3:okhttp:4.9.1' implementation 'com.squareup.okhttp3:okhttp:4.9.2'
implementation 'net.aksingh:owm-japis:2.5.3.0' implementation 'net.aksingh:owm-japis:2.5.3.0'
implementation 'net.objecthunter:exp4j:0.4.8' implementation 'net.objecthunter:exp4j:0.4.8'
@ -59,7 +66,9 @@ dependencies {
implementation 'org.jsoup:jsoup:1.14.3' implementation 'org.jsoup:jsoup:1.14.3'
implementation 'org.twitter4j:twitter4j-core:4.0.7' implementation 'org.twitter4j:twitter4j-core:4.0.7'
testImplementation 'org.assertj:assertj-core:3.21.0' testImplementation 'com.willowtreeapps.assertk:assertk-jvm:0.25'
// testImplementation 'org.mockito.kotlin:mockito-kotlin:4.0.0'
// testImplementation "org.mockito:mockito-core:4.0.0"
testImplementation 'org.testng:testng:7.4.0' testImplementation 'org.testng:testng:7.4.0'
} }
@ -117,6 +126,7 @@ jar {
manifest.attributes('Main-Class': mainClassName, manifest.attributes('Main-Class': mainClassName,
'Class-Path': '. ./lib/' + configurations.runtimeClasspath.collect { it.getName() }.join(' ./lib/')) 'Class-Path': '. ./lib/' + configurations.runtimeClasspath.collect { it.getName() }.join(' ./lib/'))
archiveVersion.set("") archiveVersion.set("")
exclude('log4j2.xml')
} }
clean { clean {

View file

@ -2,54 +2,76 @@
<SmellBaseline> <SmellBaseline>
<ManuallySuppressedIssues></ManuallySuppressedIssues> <ManuallySuppressedIssues></ManuallySuppressedIssues>
<CurrentIssues> <CurrentIssues>
<ID>ComplexMethod:EntriesMgr.kt$EntriesMgr$ fun saveEntries( bot: Mobibot, entries: List&lt;EntryLink&gt;, history: MutableList&lt;String&gt;, isDayBackup: Boolean )</ID> <ID>ComplexMethod:FeedsMgr.kt$FeedsMgr.Companion$ fun saveFeed(entries: Entries, currentFile: String = currentXml)</ID>
<ID>ComplexMethod:Weather2.kt$Weather2.Companion$ @JvmStatic @Throws(ModuleException::class) fun getWeather(query: String, apiKey: String?): List&lt;Message&gt;</ID> <ID>ComplexMethod:Weather2.kt$Weather2.Companion$ @JvmStatic @Throws(ModuleException::class) fun getWeather(query: String, apiKey: String?): List&lt;Message&gt;</ID>
<ID>LongMethod:EntriesMgr.kt$EntriesMgr$ fun saveEntries( bot: Mobibot, entries: List&lt;EntryLink&gt;, history: MutableList&lt;String&gt;, isDayBackup: Boolean )</ID> <ID>LongMethod:FeedsMgr.kt$FeedsMgr.Companion$ fun saveFeed(entries: Entries, currentFile: String = currentXml)</ID>
<ID>LongMethod:Mobibot.kt$Mobibot.Companion$ @JvmStatic fun main(args: Array&lt;String&gt;)</ID> <ID>LongMethod:Mobibot.kt$Mobibot.Companion$@Throws(Exception::class) @JvmStatic fun main(args: Array&lt;String&gt;)</ID>
<ID>LongMethod:StockQuote.kt$StockQuote.Companion$ @JvmStatic @Throws(ModuleException::class) fun getQuote(symbol: String, apiKey: String?): List&lt;Message&gt;</ID> <ID>LongMethod:StockQuote.kt$StockQuote.Companion$ @JvmStatic @Throws(ModuleException::class) fun getQuote(symbol: 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>LongMethod:Weather2.kt$Weather2.Companion$ @JvmStatic @Throws(ModuleException::class) fun getWeather(query: String, apiKey: String?): List&lt;Message&gt;</ID>
<ID>LongParameterList:Addons.kt$Addons$(sender: String, login: String, cmd: String, args: String, isOp: Boolean, isPrivate: Boolean)</ID> <ID>LongParameterList:Comment.kt$Comment$( channel: String, cmd: String, entry: EntryLink, entryIndex: Int, commentIndex: Int, event: GenericMessageEvent )</ID>
<ID>LongParameterList:Comment.kt$Comment$( bot: Mobibot, cmd: String, sender: String, isOp: Boolean, entry: EntryLink, index: Int, commentIndex: Int )</ID>
<ID>LongParameterList:Comment.kt$Comment$( bot: Mobibot, sender: String, isOp: Boolean, entry: EntryLink, index: Int, commentIndex: Int )</ID>
<ID>LongParameterList:Comment.kt$Comment$(bot: Mobibot, cmd: String, sender: String, entry: EntryLink, index: Int, commentIndex: Int)</ID>
<ID>LongParameterList:Mobibot.kt$Mobibot$( nick: String, list: List&lt;String&gt;, maxPerLine: Int, separator: String = " ", isPrivate: Boolean, isBold: Boolean = false, isIndent: Boolean = false )</ID>
<ID>LongParameterList:Twitter.kt$Twitter.Companion$( consumerKey: String?, consumerSecret: String?, token: String?, tokenSecret: String?, handle: String?, message: String, isDm: Boolean )</ID> <ID>LongParameterList:Twitter.kt$Twitter.Companion$( consumerKey: String?, consumerSecret: String?, token: String?, tokenSecret: String?, handle: String?, message: String, isDm: Boolean )</ID>
<ID>MagicNumber:Comment.kt$Comment$3</ID>
<ID>MagicNumber:CurrencyConverter.kt$CurrencyConverter$11</ID>
<ID>MagicNumber:CurrencyConverter.kt$CurrencyConverter$3</ID>
<ID>MagicNumber:CurrencyConverter.kt$CurrencyConverter.Companion$3</ID>
<ID>MagicNumber:CurrencyConverter.kt$CurrencyConverter.Companion$4</ID>
<ID>MagicNumber:CurrencyConverter.kt$CurrencyConverter.Companion$8</ID>
<ID>MagicNumber:Cycle.kt$Cycle$10</ID>
<ID>MagicNumber:Cycle.kt$Cycle$1000L</ID>
<ID>MagicNumber:Ignore.kt$Ignore$8</ID>
<ID>MagicNumber:Mobibot.kt$Mobibot$8</ID>
<ID>MagicNumber:Modules.kt$Modules$7</ID>
<ID>MagicNumber:Recap.kt$Recap.Companion$10</ID>
<ID>MagicNumber:StockQuote.kt$StockQuote.Companion$10</ID>
<ID>MagicNumber:Tell.kt$Tell$50</ID>
<ID>MagicNumber:Tell.kt$Tell$7</ID>
<ID>MagicNumber:Twitter.kt$Twitter$1000L</ID>
<ID>MagicNumber:Twitter.kt$Twitter$60L</ID>
<ID>MagicNumber:TwitterOAuth.kt$TwitterOAuth$401</ID>
<ID>MagicNumber:Users.kt$Users$8</ID>
<ID>MagicNumber:Utils.kt$Utils$30</ID>
<ID>MagicNumber:Utils.kt$Utils$365</ID>
<ID>MagicNumber:Utils.kt$Utils$7</ID>
<ID>MagicNumber:View.kt$View$6</ID>
<ID>MagicNumber:Weather2.kt$Weather2.Companion$1.60934</ID>
<ID>MagicNumber:Weather2.kt$Weather2.Companion$32</ID>
<ID>MagicNumber:Weather2.kt$Weather2.Companion$5</ID>
<ID>MagicNumber:Weather2.kt$Weather2.Companion$9</ID>
<ID>MagicNumber:WorldTime.kt$WorldTime$14</ID>
<ID>MagicNumber:WorldTime.kt$WorldTime$4</ID>
<ID>MagicNumber:WorldTime.kt$WorldTime.Companion$3</ID>
<ID>MagicNumber:WorldTime.kt$WorldTime.Companion$3600</ID>
<ID>MagicNumber:WorldTime.kt$WorldTime.Companion$60</ID>
<ID>MagicNumber:WorldTime.kt$WorldTime.Companion$86.4</ID>
<ID>NestedBlockDepth:Addons.kt$Addons$ fun add(command: AbstractCommand, props: Properties)</ID> <ID>NestedBlockDepth:Addons.kt$Addons$ fun add(command: AbstractCommand, props: Properties)</ID>
<ID>NestedBlockDepth:Calc.kt$Calc$override fun commandResponse( sender: String, cmd: String, args: String, isPrivate: Boolean )</ID> <ID>NestedBlockDepth:Comment.kt$Comment$override fun commandResponse(channel: String, args: String, event: GenericMessageEvent)</ID>
<ID>NestedBlockDepth:Comment.kt$Comment$override fun commandResponse( sender: String, login: String, args: String, isOp: Boolean, isPrivate: Boolean )</ID> <ID>NestedBlockDepth:CurrencyConverter.kt$CurrencyConverter.Companion$ @JvmStatic fun convertCurrency(query: String): Message</ID>
<ID>NestedBlockDepth:CryptoPrices.kt$CryptoPrices$ override fun run(sender: String, cmd: String, args: String, isPrivate: Boolean)</ID>
<ID>NestedBlockDepth:CurrencyConverter.kt$CurrencyConverter$ override fun run(sender: String, cmd: String, args: String, isPrivate: Boolean)</ID>
<ID>NestedBlockDepth:CurrencyConverter.kt$CurrencyConverter$override fun helpResponse(sender: String, isPrivate: Boolean): Boolean</ID>
<ID>NestedBlockDepth:CurrencyConverter.kt$CurrencyConverter.Companion$ @Suppress("MagicNumber") @JvmStatic fun convertCurrency(query: String): Message</ID>
<ID>NestedBlockDepth:EntriesMgr.kt$EntriesMgr$ @Throws(IOException::class, FeedException::class) fun loadEntries(file: String, channel: String, entries: MutableList&lt;EntryLink&gt;): String</ID>
<ID>NestedBlockDepth:EntriesMgr.kt$EntriesMgr$ fun saveEntries( bot: Mobibot, entries: List&lt;EntryLink&gt;, history: MutableList&lt;String&gt;, isDayBackup: Boolean )</ID>
<ID>NestedBlockDepth:EntriesMgr.kt$EntriesMgr$// Daily backup private fun dailyBackup( bot: Mobibot, history: MutableList&lt;String&gt; )</ID>
<ID>NestedBlockDepth:EntryLink.kt$EntryLink$ private fun setTags(tags: List&lt;String?&gt;)</ID> <ID>NestedBlockDepth:EntryLink.kt$EntryLink$ private fun setTags(tags: List&lt;String?&gt;)</ID>
<ID>NestedBlockDepth:GoogleSearch.kt$GoogleSearch$ override fun run(sender: String, cmd: String, args: String, isPrivate: Boolean)</ID> <ID>NestedBlockDepth:FeedsMgr.kt$FeedsMgr.Companion$ @Throws(IOException::class, FeedException::class) fun loadFeed(entries: Entries, currentFile: String = currentXml): String</ID>
<ID>NestedBlockDepth:LinksMgr.kt$LinksMgr$override fun commandResponse( sender: String, login: String, args: String, isOp: Boolean, isPrivate: Boolean )</ID> <ID>NestedBlockDepth:FeedsMgr.kt$FeedsMgr.Companion$ fun saveFeed(entries: Entries, currentFile: String = currentXml)</ID>
<ID>NestedBlockDepth:Lookup.kt$Lookup$override fun commandResponse( sender: String, cmd: String, args: String, isPrivate: Boolean )</ID> <ID>NestedBlockDepth:GoogleSearch.kt$GoogleSearch.Companion$ @JvmStatic @Throws(ModuleException::class) fun searchGoogle(query: String, apiKey: String?, cseKey: String?): List&lt;Message&gt;</ID>
<ID>NestedBlockDepth:Mobibot.kt$Mobibot$ fun connect()</ID> <ID>NestedBlockDepth:LinksMgr.kt$LinksMgr$override fun commandResponse(channel: String, args: String, event: GenericMessageEvent)</ID>
<ID>NestedBlockDepth:StockQuote.kt$StockQuote$ override fun run(sender: String, cmd: String, args: String, isPrivate: Boolean)</ID> <ID>NestedBlockDepth:Lookup.kt$Lookup$override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent)</ID>
<ID>NestedBlockDepth:Posting.kt$Posting$override fun commandResponse(channel: String, args: String, event: GenericMessageEvent)</ID>
<ID>NestedBlockDepth:StockQuote.kt$StockQuote.Companion$ @JvmStatic @Throws(ModuleException::class) fun getQuote(symbol: String, apiKey: String?): List&lt;Message&gt;</ID> <ID>NestedBlockDepth:StockQuote.kt$StockQuote.Companion$ @JvmStatic @Throws(ModuleException::class) fun getQuote(symbol: String, apiKey: String?): List&lt;Message&gt;</ID>
<ID>NestedBlockDepth:Tell.kt$Tell$ @JvmOverloads fun send(nickname: String, isMessage: Boolean = false)</ID> <ID>NestedBlockDepth:Tell.kt$Tell$ fun send(event: GenericUserEvent)</ID>
<ID>NestedBlockDepth:Tell.kt$Tell$// Delete message. private fun deleteMessage(sender: String, args: String, isOp: Boolean, isPrivate: Boolean)</ID> <ID>NestedBlockDepth:Tell.kt$Tell$// Delete message. private fun deleteMessage(channel: String, args: String, event: GenericMessageEvent)</ID>
<ID>NestedBlockDepth:TellMessagesMgr.kt$TellMessagesMgr$ fun save(file: String, messages: List&lt;TellMessage?&gt;?, logger: Logger)</ID> <ID>NestedBlockDepth:TellMessagesMgr.kt$TellMessagesMgr$ fun load(file: String): List&lt;TellMessage&gt;</ID>
<ID>NestedBlockDepth:TellMessagesMgr.kt$TellMessagesMgr$ fun save(file: String, messages: List&lt;TellMessage?&gt;?)</ID>
<ID>NestedBlockDepth:TwitterOAuth.kt$TwitterOAuth$ @Throws(TwitterException::class, IOException::class) @JvmStatic fun main(args: Array&lt;String&gt;)</ID> <ID>NestedBlockDepth:TwitterOAuth.kt$TwitterOAuth$ @Throws(TwitterException::class, IOException::class) @JvmStatic fun main(args: Array&lt;String&gt;)</ID>
<ID>NestedBlockDepth:Weather2.kt$Weather2$ override fun run(sender: String, cmd: String, args: String, isPrivate: Boolean)</ID> <ID>NestedBlockDepth:Weather2.kt$Weather2$ override fun run(channel: String, cmd: String, args: String, event: GenericMessageEvent)</ID>
<ID>NestedBlockDepth:Weather2.kt$Weather2.Companion$ @JvmStatic @Throws(ModuleException::class) fun getWeather(query: String, apiKey: String?): List&lt;Message&gt;</ID> <ID>NestedBlockDepth:Weather2.kt$Weather2.Companion$ @JvmStatic @Throws(ModuleException::class) fun getWeather(query: String, apiKey: String?): List&lt;Message&gt;</ID>
<ID>NestedBlockDepth:WorldTime.kt$WorldTime$override fun commandResponse( sender: String, cmd: String, args: String, isPrivate: Boolean )</ID> <ID>ReturnCount:Addons.kt$Addons$ fun exec(channel: String, cmd: String, args: String, event: GenericMessageEvent): Boolean</ID>
<ID>ReturnCount:Addons.kt$Addons$ fun exec(sender: String, login: String, cmd: String, args: String, isOp: Boolean, isPrivate: Boolean): Boolean</ID> <ID>ReturnCount:Addons.kt$Addons$ fun help(channel: String, topic: String, event: GenericMessageEvent): Boolean</ID>
<ID>ReturnCount:Addons.kt$Addons$ fun help(sender: String, topic: String, isOp: Boolean, isPrivate: Boolean): Boolean</ID> <ID>ReturnCount:ExceptionSanitizer.kt$ExceptionSanitizer$ fun ModuleException.sanitize(vararg sanitize: String): ModuleException</ID>
<ID>SwallowedException:GoogleSearchTest.kt$GoogleSearchTest$e: ModuleException</ID>
<ID>SwallowedException:StockQuoteTest.kt$StockQuoteTest$e: ModuleException</ID>
<ID>ThrowsCount:GoogleSearch.kt$GoogleSearch.Companion$ @JvmStatic @Throws(ModuleException::class) fun searchGoogle(query: String, apiKey: String?, cseKey: String?): List&lt;Message&gt;</ID> <ID>ThrowsCount:GoogleSearch.kt$GoogleSearch.Companion$ @JvmStatic @Throws(ModuleException::class) fun searchGoogle(query: String, apiKey: String?, cseKey: String?): List&lt;Message&gt;</ID>
<ID>ThrowsCount:StockQuote.kt$StockQuote.Companion$ @JvmStatic @Throws(ModuleException::class) fun getQuote(symbol: String, apiKey: String?): List&lt;Message&gt;</ID> <ID>ThrowsCount:StockQuote.kt$StockQuote.Companion$ @JvmStatic @Throws(ModuleException::class) fun getQuote(symbol: String, apiKey: String?): List&lt;Message&gt;</ID>
<ID>ThrowsCount:StockQuote.kt$StockQuote.Companion$@Throws(ModuleException::class) private fun getJsonResponse(response: String, debugMessage: String): JSONObject</ID> <ID>ThrowsCount:StockQuote.kt$StockQuote.Companion$@Throws(ModuleException::class) private fun getJsonResponse(response: String, debugMessage: String): JSONObject</ID>
<ID>ThrowsCount:Weather2.kt$Weather2.Companion$ @JvmStatic @Throws(ModuleException::class) fun getWeather(query: String, apiKey: String?): List&lt;Message&gt;</ID> <ID>ThrowsCount:Weather2.kt$Weather2.Companion$ @JvmStatic @Throws(ModuleException::class) fun getWeather(query: String, apiKey: String?): List&lt;Message&gt;</ID>
<ID>TooGenericExceptionCaught:CryptoPrices.kt$CryptoPrices$e: Exception</ID>
<ID>TooGenericExceptionCaught:Mobibot.kt$Mobibot$e: Exception</ID>
<ID>TooGenericExceptionCaught:Mobibot.kt$Mobibot$ex: 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>
<ID>TooManyFunctions:Mobibot.kt$Mobibot : PircBot</ID>
<ID>TooManyFunctions:Tell.kt$Tell : AbstractCommand</ID> <ID>TooManyFunctions:Tell.kt$Tell : AbstractCommand</ID>
</CurrentIssues> </CurrentIssues>
</SmellBaseline> </SmellBaseline>

View file

@ -1,16 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN"> <Configuration status="warn">
<Appenders> <Appenders>
<Console name="stderr" target="SYSTEM_ERR"> <Console name="stderr" target="SYSTEM_ERR">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/> <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
</Console> </Console>
<Console name="input" target="SYSTEM_OUT">
<PatternLayout pattern="%d{UNIX_MILLIS} %msg%n"/>
<Filters>
<ThresholdFilter level="warn" onMatch="DENY" onMismatch="NEUTRAL"/>
<ThresholdFilter level="debug" onMatch="ACCEPT" onMismatch="DENY"/>
</Filters>
</Console>
<Console name="output" target="SYSTEM_OUT">
<PatternLayout pattern="%d{UNIX_MILLIS} >>>%msg%n"/>
<Filters>
<ThresholdFilter level="warn" onMatch="DENY" onMismatch="NEUTRAL"/>
<ThresholdFilter level="debug" onMatch="ACCEPT" onMismatch="DENY"/>
</Filters>
</Console>
</Appenders> </Appenders>
<Loggers> <Loggers>
<Logger name="net.thauvin.erik.mobibot" level="warn" additivity="false"> <Root level="warn" additivity="false">
<AppenderRef ref="stderr"/>
</Logger>
<Root level="error">
<AppenderRef ref="stderr"/> <AppenderRef ref="stderr"/>
</Root> </Root>
<logger level="debug" name="org.pircbotx.InputParser" additivity="false">
<appender-ref ref="input"/>
<appender-ref ref="stderr" level="warn"/>
</logger>
<logger level="debug" name="org.pircbotx.output.OutputRaw" additivity="false">
<appender-ref ref="output"/>
<appender-ref ref="stderr" level="warn"/>
</logger>
<logger level="warn" name="net.thauvin.erik.mobibot" additivity="false">
<appender-ref ref="stderr"/>
</logger>
</Loggers> </Loggers>
</Configuration> </Configuration>

View file

@ -3,9 +3,13 @@ server=irc.freenode.net
#port=6667 #port=6667
login=mobibot login=mobibot
nick=mobibot nick=mobibot
#realname=mobibot
# Die command password, if any
#die=changeme
# NickServ password # NickServ password
ident=changepwd ident=changeme
#ident-nick=nickserv #ident-nick=nickserv
#ident-msg=IDENTIFY changepwd #ident-msg=IDENTIFY changepwd
@ -14,7 +18,6 @@ ignore=chanserv,nickserv
tags=mobile mobitopia tags=mobile mobitopia
tags-keywords=android ios apple google tags-keywords=android ios apple google
weblog=http://www.mobitopia.org/
feed=http://www.mobitopia.org/rss.xml feed=http://www.mobitopia.org/rss.xml
backlogs=http://www.mobitopia.org/mobibot/logs backlogs=http://www.mobitopia.org/mobibot/logs

View file

@ -32,9 +32,9 @@
package net.thauvin.erik.mobibot.modules; package net.thauvin.erik.mobibot.modules;
import net.thauvin.erik.mobibot.Mobibot;
import net.thauvin.erik.mobibot.Utils; import net.thauvin.erik.mobibot.Utils;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.pircbotx.hooks.types.GenericMessageEvent;
import java.security.SecureRandom; import java.security.SecureRandom;
@ -66,8 +66,8 @@ public final class War extends AbstractModule {
/** /**
* The default constructor. * The default constructor.
*/ */
public War(final Mobibot bot) { public War() {
super(bot); super();
commands.add(WAR_CMD); commands.add(WAR_CMD);
@ -79,10 +79,8 @@ public final class War extends AbstractModule {
* {@inheritDoc} * {@inheritDoc}
*/ */
@Override @Override
public void commandResponse(@NotNull final String sender, public void commandResponse(@NotNull final String channel, @NotNull final String cmd, @NotNull final String args,
@NotNull final String cmd, @NotNull final GenericMessageEvent event) {
@NotNull final String args,
final boolean isPrivate) {
int i; int i;
int y; int y;
@ -90,20 +88,20 @@ public final class War extends AbstractModule {
i = RANDOM.nextInt(HEARTS.length); i = RANDOM.nextInt(HEARTS.length);
y = RANDOM.nextInt(HEARTS.length); y = RANDOM.nextInt(HEARTS.length);
getBot().send(sender + " drew: " + DECK[RANDOM.nextInt(DECK.length)][i]); event.respond("you drew " + DECK[RANDOM.nextInt(DECK.length)][i]);
getBot().action("drew: " + DECK[RANDOM.nextInt(DECK.length)][y]); event.getBot().sendIRC().action(channel, "drew " + DECK[RANDOM.nextInt(DECK.length)][y]);
if (i != y) { if (i != y) {
break; break;
} }
getBot().send("This means " + bold("WAR") + '!'); event.respond("This means " + bold("WAR") + '!');
} }
if (i < y) { if (i < y) {
getBot().action("lost."); event.getBot().sendIRC().action(channel, "lost.");
} else { } else {
getBot().action("wins."); event.getBot().sendIRC().action(channel, "wins.");
} }
} }
} }

View file

@ -33,6 +33,8 @@ package net.thauvin.erik.mobibot
import net.thauvin.erik.mobibot.commands.AbstractCommand import net.thauvin.erik.mobibot.commands.AbstractCommand
import net.thauvin.erik.mobibot.modules.AbstractModule import net.thauvin.erik.mobibot.modules.AbstractModule
import org.pircbotx.hooks.events.PrivateMessageEvent
import org.pircbotx.hooks.types.GenericMessageEvent
import java.util.Properties import java.util.Properties
/** /**
@ -77,7 +79,7 @@ class Addons {
if (isEnabled()) { if (isEnabled()) {
commands.add(this) commands.add(this)
if (isVisible) { if (isVisible) {
if (isOp) { if (isOpOnly) {
ops.add(name) ops.add(name)
} else { } else {
names.add(name) names.add(name)
@ -90,17 +92,18 @@ class Addons {
/** /**
* Execute a command or module. * Execute a command or module.
*/ */
fun exec(sender: String, login: String, cmd: String, args: String, isOp: Boolean, isPrivate: Boolean): Boolean { fun exec(channel: String, cmd: String, args: String, event: GenericMessageEvent): Boolean {
for (command in commands) { val cmds = if (event is PrivateMessageEvent) commands else commands.filter { it.isPublic }
for (command in cmds) {
if (command.name.startsWith(cmd)) { if (command.name.startsWith(cmd)) {
command.commandResponse(sender, login, args, isOp, isPrivate) command.commandResponse(channel, args, event)
return true return true
} }
} }
val mods = if (isPrivate) modules.filter { it.isPrivateMsgEnabled } else modules val mods = if (event is PrivateMessageEvent) modules.filter { it.isPrivateMsgEnabled } else modules
for (module in mods) { for (module in mods) {
if (module.commands.contains(cmd)) { if (module.commands.contains(cmd)) {
module.commandResponse(sender, cmd, args, isPrivate) module.commandResponse(channel, cmd, args, event)
return true return true
} }
} }
@ -110,10 +113,10 @@ class Addons {
/** /**
* Match a command. * Match a command.
*/ */
fun match(sender: String, login: String, message: String, isOp: Boolean, isPrivate: Boolean): Boolean { fun match(channel: String, event: GenericMessageEvent): Boolean {
for (command in commands) { for (command in commands) {
if (command.matches(message)) { if (command.matches(event.message)) {
command.commandResponse(sender, login, message, isOp, isPrivate) command.commandResponse(channel, event.message, event)
return true return true
} }
} }
@ -123,15 +126,15 @@ class Addons {
/** /**
* Commands and Modules help. * Commands and Modules help.
*/ */
fun help(sender: String, topic: String, isOp: Boolean, isPrivate: Boolean): Boolean { fun help(channel: String, topic: String, event: GenericMessageEvent): Boolean {
for (command in commands) { for (command in commands) {
if (command.isVisible && command.name.startsWith(topic)) { if (command.isVisible && command.name.startsWith(topic)) {
return command.helpResponse(topic, sender, isOp, isPrivate) return command.helpResponse(channel, topic, event)
} }
} }
for (module in modules) { for (module in modules) {
if (module.commands.contains(topic)) { if (module.commands.contains(topic)) {
return module.helpResponse(sender, isPrivate) return module.helpResponse(event)
} }
} }
return false return false

View file

@ -45,11 +45,6 @@ object Constants {
*/ */
const val DEBUG_ARG = "debug" const val DEBUG_ARG = "debug"
/**
* The debug command.
*/
const val DEBUG_CMD = "debug"
/** /**
* Default IRC Port. * Default IRC Port.
*/ */
@ -58,12 +53,7 @@ object Constants {
/** /**
* Default IRC Server. * Default IRC Server.
*/ */
const val DEFAULT_SERVER = "irc.freenode.net" const val DEFAULT_SERVER = "irc.libera.chat"
/**
* The die command.
*/
const val DIE_CMD = "die"
/** /**
* Help command line argument. * Help command line argument.
@ -75,11 +65,6 @@ object Constants {
*/ */
const val HELP_CMD = "help" const val HELP_CMD = "help"
/**
* The kill command.
*/
const val KILL_CMD = "kill"
/** /**
* The link command. * The link command.
*/ */

View file

@ -36,38 +36,36 @@ import com.rometools.rome.io.SyndFeedInput
import com.rometools.rome.io.XmlReader import com.rometools.rome.io.XmlReader
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.sendMessage
import net.thauvin.erik.mobibot.entries.FeedsMgr
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.NoticeMessage
import org.pircbotx.hooks.types.GenericMessageEvent
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.io.IOException import java.io.IOException
import java.net.URL import java.net.URL
/** /**
* Reads an RSS feed. * Reads an RSS feed.
*/ */
class FeedReader( class FeedReader(private val url: String, val event: GenericMessageEvent) : Runnable {
// Bot private val logger: Logger = LoggerFactory.getLogger(FeedsMgr::class.java)
private val bot: Mobibot,
// Nick of the person who sent the message
private val sender: String,
// URL to fetch
private val url: String
) : Runnable {
/** /**
* Fetches the Feed's items. * Fetches the Feed's items.
*/ */
override fun run() { override fun run() {
with(bot) {
try { try {
readFeed(url).forEach { readFeed(url).forEach {
send(sender, it) event.sendMessage("", it)
} }
} catch (e: FeedException) { } catch (e: FeedException) {
if (logger.isDebugEnabled) logger.debug("Unable to parse the feed at $url", e) if (logger.isWarnEnabled) logger.warn("Unable to parse the feed at $url", e)
send(sender, "An error has occurred while parsing the feed: ${e.message}", false) event.sendMessage("An error has occurred while parsing the feed: ${e.message}")
} catch (e: IOException) { } catch (e: IOException) {
if (logger.isDebugEnabled) logger.debug("Unable to fetch the feed at $url", e) if (logger.isWarnEnabled) logger.warn("Unable to fetch the feed at $url", e)
send(sender, "An error has occurred while fetching the feed: ${e.message}", false) event.sendMessage("An error has occurred while fetching the feed: ${e.message}")
}
} }
} }
@ -81,11 +79,11 @@ class FeedReader(
val feed = input.build(reader) val feed = input.build(reader)
val items = feed.entries val items = feed.entries
if (items.isEmpty()) { if (items.isEmpty()) {
messages.add(PublicMessage("There is currently nothing to view.")) messages.add(NoticeMessage("There is currently nothing to view."))
} else { } else {
items.take(maxItems).forEach { items.take(maxItems).forEach {
messages.add(PublicMessage(it.title)) messages.add(NoticeMessage(it.title))
messages.add(PublicMessage(helpFormat(green(it.link), false))) messages.add(NoticeMessage(helpFormat(green(it.link), false)))
} }
} }
} }

View file

@ -29,23 +29,21 @@
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/ */
package net.thauvin.erik.mobibot package net.thauvin.erik.mobibot
import net.thauvin.erik.mobibot.Utils.appendIfMissing import net.thauvin.erik.mobibot.Utils.appendIfMissing
import net.thauvin.erik.mobibot.Utils.buildCmdSyntax import net.thauvin.erik.mobibot.Utils.bot
import net.thauvin.erik.mobibot.Utils.colorize
import net.thauvin.erik.mobibot.Utils.getIntProperty import net.thauvin.erik.mobibot.Utils.getIntProperty
import net.thauvin.erik.mobibot.Utils.helpFormat import net.thauvin.erik.mobibot.Utils.isChannelOp
import net.thauvin.erik.mobibot.Utils.sendList
import net.thauvin.erik.mobibot.Utils.sendMessage
import net.thauvin.erik.mobibot.Utils.toIsoLocalDate import net.thauvin.erik.mobibot.Utils.toIsoLocalDate
import net.thauvin.erik.mobibot.Utils.today
import net.thauvin.erik.mobibot.commands.AddLog
import net.thauvin.erik.mobibot.commands.ChannelFeed import net.thauvin.erik.mobibot.commands.ChannelFeed
import net.thauvin.erik.mobibot.commands.Cycle import net.thauvin.erik.mobibot.commands.Cycle
import net.thauvin.erik.mobibot.commands.Debug
import net.thauvin.erik.mobibot.commands.Die import net.thauvin.erik.mobibot.commands.Die
import net.thauvin.erik.mobibot.commands.Ignore import net.thauvin.erik.mobibot.commands.Ignore
import net.thauvin.erik.mobibot.commands.Info import net.thauvin.erik.mobibot.commands.Info
import net.thauvin.erik.mobibot.commands.Kill
import net.thauvin.erik.mobibot.commands.Me import net.thauvin.erik.mobibot.commands.Me
import net.thauvin.erik.mobibot.commands.Modules import net.thauvin.erik.mobibot.commands.Modules
import net.thauvin.erik.mobibot.commands.Msg import net.thauvin.erik.mobibot.commands.Msg
@ -61,8 +59,6 @@ import net.thauvin.erik.mobibot.commands.links.Posting
import net.thauvin.erik.mobibot.commands.links.Tags import net.thauvin.erik.mobibot.commands.links.Tags
import net.thauvin.erik.mobibot.commands.links.View import net.thauvin.erik.mobibot.commands.links.View
import net.thauvin.erik.mobibot.commands.tell.Tell import net.thauvin.erik.mobibot.commands.tell.Tell
import net.thauvin.erik.mobibot.entries.EntriesMgr
import net.thauvin.erik.mobibot.entries.EntryLink
import net.thauvin.erik.mobibot.modules.Calc import net.thauvin.erik.mobibot.modules.Calc
import net.thauvin.erik.mobibot.modules.CryptoPrices import net.thauvin.erik.mobibot.modules.CryptoPrices
import net.thauvin.erik.mobibot.modules.CurrencyConverter import net.thauvin.erik.mobibot.modules.CurrencyConverter
@ -73,12 +69,9 @@ import net.thauvin.erik.mobibot.modules.Lookup
import net.thauvin.erik.mobibot.modules.Ping import net.thauvin.erik.mobibot.modules.Ping
import net.thauvin.erik.mobibot.modules.RockPaperScissors import net.thauvin.erik.mobibot.modules.RockPaperScissors
import net.thauvin.erik.mobibot.modules.StockQuote import net.thauvin.erik.mobibot.modules.StockQuote
import net.thauvin.erik.mobibot.modules.Twitter
import net.thauvin.erik.mobibot.modules.War import net.thauvin.erik.mobibot.modules.War
import net.thauvin.erik.mobibot.modules.Weather2 import net.thauvin.erik.mobibot.modules.Weather2
import net.thauvin.erik.mobibot.modules.WorldTime import net.thauvin.erik.mobibot.modules.WorldTime
import net.thauvin.erik.mobibot.msg.Message
import net.thauvin.erik.pinboard.PinboardPoster
import net.thauvin.erik.semver.Version import net.thauvin.erik.semver.Version
import org.apache.commons.cli.CommandLine import org.apache.commons.cli.CommandLine
import org.apache.commons.cli.CommandLineParser import org.apache.commons.cli.CommandLineParser
@ -87,10 +80,19 @@ import org.apache.commons.cli.HelpFormatter
import org.apache.commons.cli.Option import org.apache.commons.cli.Option
import org.apache.commons.cli.Options import org.apache.commons.cli.Options
import org.apache.commons.cli.ParseException import org.apache.commons.cli.ParseException
import org.apache.logging.log4j.Level import org.pircbotx.Configuration
import org.apache.logging.log4j.LogManager import org.pircbotx.PircBotX
import org.apache.logging.log4j.Logger import org.pircbotx.hooks.ListenerAdapter
import org.jibble.pircbot.PircBot import org.pircbotx.hooks.events.ActionEvent
import org.pircbotx.hooks.events.DisconnectEvent
import org.pircbotx.hooks.events.JoinEvent
import org.pircbotx.hooks.events.MessageEvent
import org.pircbotx.hooks.events.NickChangeEvent
import org.pircbotx.hooks.events.PartEvent
import org.pircbotx.hooks.events.PrivateMessageEvent
import org.pircbotx.hooks.types.GenericMessageEvent
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.io.BufferedOutputStream import java.io.BufferedOutputStream
import java.io.File import java.io.File
import java.io.FileNotFoundException import java.io.FileNotFoundException
@ -100,427 +102,139 @@ import java.io.PrintStream
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Paths import java.nio.file.Paths
import java.util.Properties import java.util.Properties
import java.util.Timer
import java.util.logging.ConsoleHandler
import java.util.regex.Pattern import java.util.regex.Pattern
import kotlin.system.exitProcess import kotlin.system.exitProcess
/**
* Implements the #mobitopia bot.
*/
@Version(properties = "version.properties", className = "ReleaseInfo", template = "version.mustache", type = "kt") @Version(properties = "version.properties", className = "ReleaseInfo", template = "version.mustache", type = "kt")
class Mobibot(nickname: String, channel: String, logsDirPath: String, p: Properties) : PircBot() { class Mobibot(nickname: String, channel: String, logsDirPath: String, p: Properties) : ListenerAdapter() {
// The bot configuration.
private val config: Configuration
// Commands and Modules // Commands and Modules
private val addons = Addons() private val addons = Addons()
// Tell module
private val tell: Tell
/** Main channel. */ /** Main channel. */
val channel: String val channel: String
// IRC port
private val ircPort: Int
/** IRC server. */
val ircServer: String
/** Logger. */ /** Logger. */
val logger: Logger = LogManager.getLogger(Mobibot::class.java) val logger: Logger = LoggerFactory.getLogger(Mobibot::class.java)
/** Logger default level. */
val loggerLevel: Level
/** Log directory. */
val logsDir: String
// Pinboard posts handler
private val pinboard: PinboardPoster = PinboardPoster()
/** Tell command. */
val tell: Tell
/** Today's date. */
val today = today()
/** Twitter module. */
val twitter: Twitter
/** The backlogs URL. */
val backlogsUrl: String
// Ident message
private val identMsg: String
// Ident nick
private val identNick: String
// NickServ ident password
private val identPwd: String
// Is pinboard enabled?
private var isPinboardEnabled = false
/** Timer. */
val timer = Timer(true)
/** Weblog URL */
val weblogUrl: String
/** The current channel name. */
private val channelName: String
get() = channel.substring(1)
/** The enabled modules names. */
val modulesNames: List<String>
get() = addons.modulesNames
/**
* Sends an action to the current channel.
*/
fun action(action: String) {
action(channel, action)
}
/**
* Sends an action to the channel.
*/
private fun action(channel: String, action: String) {
if (channel.isNotBlank() && action.isNotBlank()) {
sendAction(channel, action)
}
}
/**
* Adds pin on pinboard.
*/
fun addPin(entry: EntryLink) {
if (isPinboardEnabled) {
PinboardUtils.addPin(pinboard, ircServer, entry)
}
}
/** /**
* Connects to the server and joins the channel. * Connects to the server and joins the channel.
*/ */
fun connect() { fun connect() {
try { PircBotX(config).startBot()
connect(ircServer, ircPort)
} catch (e: Exception) {
if (logger.isDebugEnabled) {
logger.debug("Unable to connect to $ircServer, will try again.", e)
}
var retries = 0
while (retries++ < MAX_RECONNECT && !isConnected) {
@Suppress("MagicNumber")
sleep(10)
try {
connect(ircServer, ircPort)
} catch (ex: Exception) {
if (retries == MAX_RECONNECT) {
if (logger.isDebugEnabled) {
logger.debug("Unable to reconnect to $ircServer, after $MAX_RECONNECT retries.", ex)
}
System.err.println("An error has occurred while reconnecting; ${ex.message}")
exitProcess(1)
}
}
}
}
identify()
joinChannel()
}
/**
* Deletes pin on pinboard.
*/
fun deletePin(index: Int, entry: EntryLink) {
if (isPinboardEnabled) {
PinboardUtils.deletePin(pinboard, entry)
}
if (twitter.isAutoPost) {
twitter.removeEntry(index)
}
} }
/** /**
* Responds with the default help. * Responds with the default help.
*/ */
@Suppress("MagicNumber") private fun helpDefault(event: GenericMessageEvent) {
fun helpDefault(sender: String, isOp: Boolean, isPrivate: Boolean) { event.sendMessage("Type a URL on $channel to post it.")
send(sender, "Type a URL on $channel to post it.", isPrivate) event.sendMessage("For more information on a specific command, type:")
send(sender, "For more information on a specific command, type:", isPrivate) event.sendMessage(
send( Utils.helpFormat(
sender, Utils.buildCmdSyntax(
helpFormat(buildCmdSyntax("%c ${Constants.HELP_CMD} <command>", nick, isPrivate)), "%c ${Constants.HELP_CMD} <command>",
isPrivate event.bot().nick,
event is PrivateMessageEvent
) )
send(sender, "The commands are:", isPrivate) ),
sendList(sender, addons.names, 8, isPrivate = isPrivate, isBold = true, isIndent = true) )
if (isOp) { event.sendMessage("The commands are:")
send(sender, "The op commands are:", isPrivate) event.sendList(addons.names, 8, isBold = true, isIndent = true)
sendList(sender, addons.ops, 8, isPrivate = isPrivate, isBold = true, isIndent = true) if (isChannelOp(channel, event)) {
event.sendMessage("The op commands are:")
event.sendList(addons.ops, 8, isBold = true, isIndent = true)
} }
} }
/** /**
* Responds with the default, commands or modules help. * Responds with the default, commands or modules help.
*/ */
private fun helpResponse(sender: String, topic: String, isPrivate: Boolean) { private fun helpResponse(event: GenericMessageEvent, topic: String) {
val isOp = isOp(sender) if (topic.isBlank() || !addons.help(channel, topic.lowercase().trim(), event)) {
if (topic.isBlank() || !addons.help(sender, topic.lowercase().trim(), isOp, isPrivate)) { helpDefault(event)
helpDefault(sender, isOp, isPrivate)
} }
} }
/** override fun onAction(event: ActionEvent?) {
* Identifies the bot. if (channel == event?.channel?.name) {
*/ storeRecap(event.user!!.nick, event.action, true)
private fun identify() {
// Identify with NickServ
if (identPwd.isNotBlank()) {
identify(identPwd)
}
// Identify with a specified nick
if (identNick.isNotBlank() && identMsg.isNotBlank()) {
sendMessage(identNick, identMsg)
} }
} }
/** override fun onDisconnect(event: DisconnectEvent?) {
* Returns {@code true} if the specified sender is an Op on the [channel][.ircChannel]. with(event!!.getBot<PircBotX>()) {
*/ LinksMgr.twitter.notification("$nick disconnected from irc://$serverHostname")
fun isOp(sender: String): Boolean {
for (user in getUsers(channel)) {
if (user.nick == sender) {
return user.isOp
} }
} LinksMgr.twitter.shutdown()
return false
} }
/** override fun onPrivateMessage(event: PrivateMessageEvent?) {
* Joins the bot's channel. if (logger.isTraceEnabled) logger.trace("<<< ${event!!.user!!.nick}: ${event.message}")
*/ val cmds = event!!.message.trim().split(" ".toRegex(), 2)
private fun joinChannel() { val cmd = cmds[0].lowercase()
joinChannel(channel) val args = if (cmds.size > 1) {
twitter.notification("$name ${ReleaseInfo.VERSION} has joined $channel") cmds[1].trim()
} else ""
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 onDisconnect() { override fun onJoin(event: JoinEvent?) {
if (weblogUrl.isNotBlank()) { with(event!!.getBot<PircBotX>()) {
version = weblogUrl if (event.user!!.nick == nick) {
LinksMgr.twitter.notification("$nick has joined ${event.channel.name} on irc://$serverHostname")
} else {
tell.send(event)
}
} }
@Suppress("MagicNumber")
sleep(5)
connect()
} }
override fun onMessage( override fun onMessage(event: MessageEvent?) {
channel: String, val sender = event!!.user!!.nick
sender: String, val message = event.message
login: String, tell.send(event)
hostname: String, if (message.matches("(?i)${Pattern.quote(event.bot().nick)}:.*".toRegex())) { // mobibot: <command>
message: String if (logger.isTraceEnabled) logger.trace(">>> $sender: $message")
) {
if (logger.isDebugEnabled) logger.debug(">>> $sender: $message")
tell.send(sender, true)
if (message.matches("(?i)${Pattern.quote(nick)}:.*".toRegex())) { // mobibot: <command>
val cmds = message.substring(message.indexOf(':') + 1).trim().split(" ".toRegex(), 2) val cmds = message.substring(message.indexOf(':') + 1).trim().split(" ".toRegex(), 2)
val cmd = cmds[0].lowercase() val cmd = cmds[0].lowercase()
val args = if (cmds.size > 1) { val args = if (cmds.size > 1) {
cmds[1].trim() cmds[1].trim()
} else "" } else ""
if (cmd.startsWith(Constants.HELP_CMD)) { // mobibot: help if (cmd.startsWith(Constants.HELP_CMD)) { // mobibot: help
helpResponse(sender, args, false) helpResponse(event, args)
} else { } else {
// Execute module or command // Execute module or command
addons.exec(sender, login, cmd, args, isOp(sender), false) addons.exec(channel, cmd, args, event)
} }
} else { } else if (addons.match(channel, event)) { // Links, e.g.: https://www.example.com/ or L1: , etc.
// Links, e.g.: https://www.example.com/ or L1: , etc. if (logger.isTraceEnabled) logger.trace(">>> $sender: $message")
addons.match(sender, login, message, isOp(sender), false)
} }
storeRecap(sender, message, false) storeRecap(sender, message, false)
} }
override fun onPrivateMessage( override fun onNickChange(event: NickChangeEvent?) {
sender: String, tell.send(event!!)
login: String,
hostname: String,
message: String
) {
if (logger.isDebugEnabled) logger.debug(">>> $sender : $message")
val cmds = message.trim().split(" ".toRegex(), 2)
val cmd = cmds[0].lowercase()
val args = if (cmds.size > 1) {
cmds[1].trim()
} else ""
val isOp = isOp(sender)
if (cmd.startsWith(Constants.HELP_CMD)) { // help
helpResponse(sender, args, true)
} else if (isOp && Constants.KILL_CMD == cmd) { // kill
twitter.notification("$name killed by $sender on $channel")
sendRawLine("QUIT :Poof!")
exitProcess(0)
} else if (isOp && Constants.DIE_CMD == cmd) { // die
send("$sender has just signed my death sentence.")
timer.cancel()
twitter.shutdown()
twitter.notification("$name stopped by $sender on $channel")
@Suppress("MagicNumber")
sleep(3)
quitServer("The Bot Is Out There!")
exitProcess(0)
} else if (!addons.exec(sender, login, cmd, args, isOp, true)) { // Execute command or module
helpDefault(sender, isOp, true)
}
} }
override fun onAction(sender: String, login: String, hostname: String, target: String, action: String) { override fun onPart(event: PartEvent?) {
if (channel == target) { with(event!!.getBot<PircBotX>()) {
storeRecap(sender, action, true) if (event.user!!.nick == nick) {
LinksMgr.twitter.notification("$nick has left ${event.channel.name} on irc://$serverHostname")
} }
} }
override fun onJoin(channel: String, sender: String, login: String, hostname: String) {
tell.send(sender)
}
override fun onNickChange(oldNick: String, login: String, hostname: String, newNick: String) {
tell.send(newNick)
}
/**
* Sends a private message or notice.
*/
fun send(sender: String, message: String?, isPrivate: Boolean) {
if (message != null && sender.isNotBlank()) {
if (isPrivate) {
if (logger.isDebugEnabled) logger.debug("Sending message to $sender : $message")
sendMessage(sender, message)
} else {
if (logger.isDebugEnabled) logger.debug("Sending notice to $sender: $message")
sendNotice(sender, message)
}
}
}
/**
* Sends a notice to the channel.
*/
fun send(notice: String?) {
notice?.let { send(channel, it, false) }
}
/**
* Sends a message.
*/
fun send(who: String, message: Message) {
send(if (message.isNotice) who else channel, message.msg, message.color, message.isPrivate)
}
/**
* Sends a message.
*/
fun send(who: String, message: String, color: String, isPrivate: Boolean) {
send(who, colorize(message, color), isPrivate)
}
/**
* Send a formatted commands/modules, etc. list.
*/
@JvmOverloads
fun sendList(
nick: String,
list: List<String>,
maxPerLine: Int,
separator: String = " ",
isPrivate: Boolean,
isBold: Boolean = false,
isIndent: Boolean = false
) {
var i = 0
while (i < list.size) {
send(
nick,
helpFormat(
list.subList(i, list.size.coerceAtMost(i + maxPerLine)).joinToString(separator, truncated = ""),
isBold,
isIndent
),
isPrivate
)
i += maxPerLine
}
}
/**
* Sets the pinboard authentication.
*/
private fun setPinboardAuth(apiToken: String) {
if (apiToken.isNotBlank()) {
pinboard.apiToken = apiToken
isPinboardEnabled = true
if (logger.isDebugEnabled) {
val consoleHandler = ConsoleHandler()
consoleHandler.level = java.util.logging.Level.FINE
pinboard.logger.addHandler(consoleHandler)
pinboard.logger.level = java.util.logging.Level.FINE
}
}
}
/**
* Shutdown the bot.
*/
fun shutdown(sender: String, now: Boolean = false) {
if (now) { // kill
twitter.notification("$name killed by $sender on $channel")
sendRawLine("QUIT :Poof!")
} else {
timer.cancel()
twitter.shutdown()
twitter.notification("$name stopped by $sender on $channel")
@Suppress("MagicNumber")
sleep(3)
quitServer("The Bot Is Out There!")
}
exitProcess(0)
}
/**
* Sleeps for the specified number of seconds.
*/
fun sleep(secs: Int) {
try {
@Suppress("MagicNumber")
Thread.sleep(secs * 1000L)
} catch (ignore: InterruptedException) {
// Do nothing
}
}
/**
* Updates pin on pinboard.
*/
fun updatePin(oldUrl: String, entry: EntryLink) {
if (isPinboardEnabled) {
PinboardUtils.updatePin(pinboard, ircServer, oldUrl, entry)
}
}
/**
* Returns the bot's version.
*/
override fun onVersion(sourceNick: String, sourceLogin: String, sourceHostname: String, target: String) {
sendRawLine("NOTICE $sourceNick :\u0001VERSION ${ReleaseInfo.PROJECT} ${ReleaseInfo.VERSION}\u0001")
} }
companion object { companion object {
// Maximum number of times the bot will try to reconnect, if disconnected @Throws(Exception::class)
private const val MAX_RECONNECT = 10
/**
* The Truth is Out There!
*/
@JvmStatic @JvmStatic
fun main(args: Array<String>) { fun main(args: Array<String>) {
// Set up the command line options // Set up the command line options
@ -593,7 +307,7 @@ class Mobibot(nickname: String, channel: String, logsDirPath: String, p: Propert
val stdout = PrintStream( val stdout = PrintStream(
BufferedOutputStream( BufferedOutputStream(
FileOutputStream( FileOutputStream(
logsDir + channel.substring(1) + '.' + today() + ".log", true logsDir + channel.substring(1) + '.' + Utils.today() + ".log", true
) )
), true ), true
) )
@ -615,11 +329,8 @@ class Mobibot(nickname: String, channel: String, logsDirPath: String, p: Propert
} }
} }
// Create the bot // Start the bot
val bot = Mobibot(nickname, channel, logsDir, p) Mobibot(nickname, channel, logsDir, p).connect()
// Connect
bot.connect()
} }
} }
} }
@ -629,97 +340,93 @@ class Mobibot(nickname: String, channel: String, logsDirPath: String, p: Propert
* Initialize the bot. * Initialize the bot.
*/ */
init { init {
System.getProperties().setProperty("sun.net.client.defaultConnectTimeout", Constants.CONNECT_TIMEOUT.toString())
System.getProperties().setProperty("sun.net.client.defaultReadTimeout", Constants.CONNECT_TIMEOUT.toString())
name = nickname
ircServer = p.getProperty("server", Constants.DEFAULT_SERVER)
ircPort = p.getIntProperty("port", Constants.DEFAULT_PORT)
this.channel = channel this.channel = channel
logsDir = logsDirPath val ircServer = p.getProperty("server", Constants.DEFAULT_SERVER)
config = Configuration.Builder().apply {
name = nickname
login = p.getProperty("login", nickname)
realName = p.getProperty("realname", nickname)
addServer(
ircServer,
p.getIntProperty("port", Constants.DEFAULT_PORT)
)
addAutoJoinChannel(channel)
addListener(this@Mobibot)
version = "${ReleaseInfo.PROJECT} ${ReleaseInfo.VERSION}"
isAutoNickChange = true
val identPwd = p.getProperty("ident")
if (!identPwd.isNullOrBlank()) {
nickservPassword = identPwd
}
val identNick = p.getProperty("ident-nick")
if (!identNick.isNullOrBlank()) {
nickservNick = identNick
}
val identMsg = p.getProperty("ident-msg")
if (!identMsg.isNullOrBlank()) {
nickservCustomMessage = identMsg
}
isAutoReconnect = true
//socketConnectTimeout = Constants.CONNECT_TIMEOUT
//socketTimeout = Constants.CONNECT_TIMEOUT
//messageDelay = StaticDelay(500)
}.buildConfiguration()
// Store the default logger level // Load the current entries
loggerLevel = logger.level with(LinksMgr) {
entries.channel = channel
entries.ircServer = ircServer
entries.logsDir = logsDirPath
entries.backlogs = p.getProperty("backlogs", "")
entries.load()
setVerbose(true) // Set up pinboard
setAutoNickChange(true) pinboard.setApiToken(p.getProperty("pinboard-api-token", ""))
login = p.getProperty("login", name)
// Set the real name
version = ReleaseInfo.PROJECT
// setMessageDelay(1000);
// Set NICKSERV identification
identPwd = p.getProperty("ident", "")
identNick = p.getProperty("ident-nick", "")
identMsg = p.getProperty("ident-msg", "")
// Set the URLs
weblogUrl = p.getProperty("weblog", "")
backlogsUrl = p.getProperty("backlogs", weblogUrl).appendIfMissing('/')
// Load the current entries and backlogs, if any
try {
LinksMgr.startup(logsDir + EntriesMgr.CURRENT_XML, logsDir + EntriesMgr.NAV_XML, this.channel)
if (logger.isDebugEnabled) logger.debug("Last feed: ${LinksMgr.startDate}")
} catch (e: Exception) {
if (logger.isErrorEnabled) logger.error("An error occurred while loading the logs.", e)
} }
// Set the pinboard authentication
setPinboardAuth(p.getProperty("pinboard-api-token", ""))
// Load the commands // Load the commands
addons.add(AddLog(this), p) addons.add(ChannelFeed(channel.removePrefix("#")), p)
addons.add(ChannelFeed(this, channelName), p) addons.add(Comment(), p)
addons.add(Cycle(this), p) addons.add(Cycle(), p)
addons.add(Die(this), p) addons.add(Die(), p)
addons.add(Debug(this), p) addons.add(Ignore(), p)
addons.add(Ignore(this), p) addons.add(LinksMgr(), p)
addons.add(Info(this), p) addons.add(Me(), p)
addons.add(Kill(this), p) addons.add(Msg(), p)
addons.add(Me(this), p) addons.add(Nick(), p)
addons.add(Modules(this), p) addons.add(Posting(), p)
addons.add(Msg(this), p) addons.add(Recap(), p)
addons.add(Nick(this), p) addons.add(Say(), p)
addons.add(Recap(this), p) addons.add(Tags(), p)
addons.add(Say(this), p)
addons.add(Users(this), p)
addons.add(Versions(this), p)
// Tell command // Tell command
tell = Tell(this) tell = Tell("${logsDirPath}${nickname}.ser")
addons.add(tell, p) addons.add(tell, p)
// Load the links commands addons.add(LinksMgr.twitter, p)
addons.add(Comment(this), p) addons.add(Users(), p)
addons.add(Posting(this), p) addons.add(Versions(), p)
addons.add(Tags(this), p) addons.add(View(), p)
addons.add(LinksMgr(this), p)
addons.add(View(this), p)
// Load the modules // Load the modules
addons.add(Calc(this), p) addons.add(Calc(), p)
addons.add(CryptoPrices(this), p) addons.add(CryptoPrices(), p)
addons.add(CurrencyConverter(this), p) addons.add(CurrencyConverter(), p)
addons.add(Dice(this), p) addons.add(Dice(), p)
addons.add(GoogleSearch(this), p) addons.add(GoogleSearch(), p)
addons.add(Joke(this), p) addons.add(Info(tell), p)
addons.add(Lookup(this), p) addons.add(Joke(), p)
addons.add(Ping(this), p) addons.add(Lookup(), p)
addons.add(RockPaperScissors(this), p) addons.add(Modules(addons.modulesNames), p)
addons.add(StockQuote(this), p) addons.add(Ping(), p)
addons.add(War(this), p) addons.add(RockPaperScissors(), p)
addons.add(Weather2(this), p) addons.add(StockQuote(), p)
addons.add(WorldTime(this), p) addons.add(Weather2(), p)
addons.add(WorldTime(), p)
// Twitter module addons.add(War(), p)
twitter = Twitter(this)
addons.add(twitter, p)
// Sort the addons // Sort the addons
addons.sort() addons.sort()
}
}
// Save the entries
LinksMgr.saveEntries(this, true)
}
}

View file

@ -1,5 +1,5 @@
/* /*
* PinboardUtils.kt * Pinboard.kt
* *
* Copyright (c) 2004-2021, Erik C. Thauvin (erik@thauvin.net) * Copyright (c) 2004-2021, Erik C. Thauvin (erik@thauvin.net)
* All rights reserved. * All rights reserved.
@ -45,12 +45,14 @@ import java.util.Date
/** /**
* Handles posts to pinboard.in. * Handles posts to pinboard.in.
*/ */
object PinboardUtils { class Pinboard {
private val poster = PinboardPoster()
/** /**
* Adds a pin. * Adds a pin.
*/ */
@JvmStatic fun addPin(ircServer: String, entry: EntryLink) {
fun addPin(poster: PinboardPoster, ircServer: String, entry: EntryLink) { if (poster.apiToken.isNotBlank()) {
runBlocking { runBlocking {
launch { launch {
poster.addPin( poster.addPin(
@ -63,46 +65,40 @@ object PinboardUtils {
} }
} }
} }
}
/**
* Sets the pinboard API token.
*/
fun setApiToken(apiToken: String) {
poster.apiToken = apiToken
}
/** /**
* Deletes a pin. * Deletes a pin.
*/ */
@JvmStatic fun deletePin(entry: EntryLink) {
fun deletePin(poster: PinboardPoster, entry: EntryLink) { if (poster.apiToken.isNotBlank()) {
runBlocking { runBlocking {
launch { launch {
poster.deletePin(entry.link) poster.deletePin(entry.link)
} }
} }
} }
}
/** /**
* Updates a pin. * Updates a pin.
*/ */
@JvmStatic fun updatePin(ircServer: String, oldUrl: String, entry: EntryLink) {
fun updatePin(poster: PinboardPoster, ircServer: String, oldUrl: String, entry: EntryLink) { if (poster.apiToken.isNotBlank()) {
runBlocking { runBlocking {
launch { launch {
with(entry) { with(entry) {
if (oldUrl != link) { if (oldUrl != link) {
poster.deletePin(oldUrl) poster.deletePin(oldUrl)
poster.addPin( }
link, poster.addPin(link, title, entry.postedBy(ircServer), pinboardTags, date.toTimestamp())
title,
entry.postedBy(ircServer),
pinboardTags,
date.toTimestamp()
)
} else {
poster.addPin(
link,
title,
entry.postedBy(ircServer),
pinboardTags,
date.toTimestamp(),
replace = true,
shared = true
)
} }
} }
} }
@ -112,8 +108,7 @@ object PinboardUtils {
/** /**
* Format a date to a UTC timestamp. * Format a date to a UTC timestamp.
*/ */
@JvmStatic private fun Date.toTimestamp(): String {
fun Date.toTimestamp(): String {
return ZonedDateTime.ofInstant( return ZonedDateTime.ofInstant(
this.toInstant().truncatedTo(ChronoUnit.SECONDS), this.toInstant().truncatedTo(ChronoUnit.SECONDS),
ZoneId.systemDefault() ZoneId.systemDefault()

View file

@ -95,7 +95,6 @@ object TwitterOAuth {
""".trimIndent() """.trimIndent()
) )
} catch (te: TwitterException) { } catch (te: TwitterException) {
@Suppress("MagicNumber")
if (401 == te.statusCode) { if (401 == te.statusCode) {
println("Unable to get the access token.") println("Unable to get the access token.")
} else { } else {

View file

@ -32,10 +32,11 @@
package net.thauvin.erik.mobibot package net.thauvin.erik.mobibot
import net.thauvin.erik.mobibot.modules.Twitter
import java.util.TimerTask import java.util.TimerTask
class TwitterTimer(var bot: Mobibot, private var index: Int) : TimerTask() { class TwitterTimer(private var twitter: Twitter, private var index: Int) : TimerTask() {
override fun run() { override fun run() {
bot.twitter.postEntry(index) twitter.postEntry(index)
} }
} }

View file

@ -31,9 +31,13 @@
*/ */
package net.thauvin.erik.mobibot package net.thauvin.erik.mobibot
import net.thauvin.erik.mobibot.msg.Message
import net.thauvin.erik.mobibot.msg.Message.Companion.DEFAULT_COLOR import net.thauvin.erik.mobibot.msg.Message.Companion.DEFAULT_COLOR
import org.jibble.pircbot.Colors
import org.jsoup.Jsoup import org.jsoup.Jsoup
import org.pircbotx.Colors
import org.pircbotx.PircBotX
import org.pircbotx.hooks.events.PrivateMessageEvent
import org.pircbotx.hooks.types.GenericMessageEvent
import java.io.BufferedReader import java.io.BufferedReader
import java.io.IOException import java.io.IOException
import java.io.InputStreamReader import java.io.InputStreamReader
@ -85,6 +89,13 @@ object Utils {
@JvmStatic @JvmStatic
fun bold(s: String?): String = colorize(s, Colors.BOLD) fun bold(s: String?): String = colorize(s, Colors.BOLD)
/**
* Returns the [PircBotX] instance.
*/
fun GenericMessageEvent.bot(): PircBotX {
return getBot() as PircBotX
}
/** /**
* Build a help command by replacing `%c` with the bot's pub/priv command, and `%n` with the bot's * Build a help command by replacing `%c` with the bot's pub/priv command, and `%n` with the bot's
* nick. * nick.
@ -159,6 +170,14 @@ object Utils {
return if (isIndent) s.prependIndent() else s return if (isIndent) s.prependIndent() else s
} }
/**
* Returns {@code true} if the specified user is an operator on the [channel].
*/
@JvmStatic
fun isChannelOp(channel: String, event: GenericMessageEvent): Boolean {
return event.bot().userChannelDao.getChannel(channel).isOp(event.user)
}
/** /**
* Obfuscates the given string. * Obfuscates the given string.
*/ */
@ -203,6 +222,56 @@ object Utils {
@JvmStatic @JvmStatic
fun reverseColor(s: String?): String = colorize(s, Colors.REVERSE) fun reverseColor(s: String?): String = colorize(s, Colors.REVERSE)
/**
* Send a formatted commands/modules, etc. list.
*/
@JvmStatic
fun GenericMessageEvent.sendList(
list: List<String>,
maxPerLine: Int,
separator: String = " ",
isBold: Boolean = false,
isIndent: Boolean = false
) {
var i = 0
while (i < list.size) {
sendMessage(
helpFormat(
list.subList(i, list.size.coerceAtMost(i + maxPerLine)).joinToString(separator, truncated = ""),
isBold,
isIndent
),
)
i += maxPerLine
}
}
/**
* Sends a [message].
*/
@JvmStatic
fun GenericMessageEvent.sendMessage(channel: String, message: Message) {
if (message.isNotice) {
bot().sendIRC().notice(user.nick, colorize(message.msg, message.color))
} else if (message.isPrivate || this is PrivateMessageEvent || channel.isBlank()) {
respondPrivateMessage(colorize(message.msg, message.color))
} else {
bot().sendIRC().message(channel, colorize(message.msg, message.color))
}
}
/**
* Sends a response as a private message or notice.
*/
@JvmStatic
fun GenericMessageEvent.sendMessage(message: String) {
if (this is PrivateMessageEvent) {
respondPrivateMessage(message)
} else {
bot().sendIRC().notice(user.nick, message)
}
}
/** /**
* Returns today's date. * Returns today's date.
*/ */
@ -258,7 +327,6 @@ object Utils {
/** /**
* Converts milliseconds to year month week day hour and minutes. * Converts milliseconds to year month week day hour and minutes.
*/ */
@Suppress("MagicNumber")
@JvmStatic @JvmStatic
fun uptime(uptime: Long): String { fun uptime(uptime: Long): String {
val info = StringBuilder() val info = StringBuilder()

View file

@ -32,31 +32,31 @@
package net.thauvin.erik.mobibot.commands package net.thauvin.erik.mobibot.commands
import net.thauvin.erik.mobibot.Mobibot import net.thauvin.erik.mobibot.Utils.bot
import net.thauvin.erik.mobibot.Utils.buildCmdSyntax import net.thauvin.erik.mobibot.Utils.buildCmdSyntax
import net.thauvin.erik.mobibot.Utils.isChannelOp
import net.thauvin.erik.mobibot.Utils.sendMessage
import org.pircbotx.hooks.events.PrivateMessageEvent
import org.pircbotx.hooks.types.GenericMessageEvent
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
abstract class AbstractCommand(val bot: Mobibot) { abstract class AbstractCommand {
abstract val name: String abstract val name: String
abstract val help: List<String> abstract val help: List<String>
abstract val isOp: Boolean abstract val isOpOnly: Boolean
abstract val isPublic: Boolean abstract val isPublic: Boolean
abstract val isVisible: Boolean abstract val isVisible: Boolean
val properties: MutableMap<String, String> = ConcurrentHashMap() val properties: MutableMap<String, String> = ConcurrentHashMap()
abstract fun commandResponse( abstract fun commandResponse(channel: String, args: String, event: GenericMessageEvent)
sender: String,
login: String,
args: String,
isOp: Boolean,
isPrivate: Boolean
)
open fun helpResponse(command: String, sender: String, isOp: Boolean, isPrivate: Boolean): Boolean { open fun helpResponse(channel: String, topic: String, event: GenericMessageEvent): Boolean {
if (!this.isOp || this.isOp == isOp) { if (!isOpOnly || isOpOnly == isChannelOp(channel, event)) {
for (h in help) { for (h in help) {
bot.send(sender, buildCmdSyntax(h, bot.nick, isPrivate), isPrivate) event.sendMessage(
buildCmdSyntax(h, event.bot().nick, event is PrivateMessageEvent || !isPublic),
)
} }
return true return true
} }

View file

@ -1,70 +0,0 @@
/*
* AddLog.kt
*
* Copyright (c) 2004-2021, Erik C. Thauvin (erik@thauvin.net)
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* Neither the name of this project nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.thauvin.erik.mobibot.commands
import net.thauvin.erik.mobibot.Mobibot
import net.thauvin.erik.mobibot.commands.links.LinksMgr.Companion.history
import net.thauvin.erik.mobibot.entries.EntriesMgr
import java.io.File
class AddLog(bot: Mobibot) : AbstractCommand(bot) {
override val name = "addlog"
override val help = emptyList<String>()
override val isOp = true
override val isPublic = false
override val isVisible = false
override fun commandResponse(
sender: String,
login: String,
args: String,
isOp: Boolean,
isPrivate: Boolean
) {
if (isOp) {
if (args.isNotBlank()) {
// e.g: 2014-04-01
val backlog = File("${bot.logsDir}$args${EntriesMgr.XML_EXT}")
if (backlog.exists()) {
history.add(0, args)
} else {
bot.send(sender, "The specified log could not be found.", isPrivate)
return
}
}
@Suppress("MagicNumber")
bot.sendList(sender, history, 4, isPrivate = isPrivate, isIndent = true)
}
}
}

View file

@ -35,13 +35,14 @@ package net.thauvin.erik.mobibot.commands
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import net.thauvin.erik.mobibot.FeedReader import net.thauvin.erik.mobibot.FeedReader
import net.thauvin.erik.mobibot.Mobibot
import net.thauvin.erik.mobibot.Utils.helpFormat import net.thauvin.erik.mobibot.Utils.helpFormat
import net.thauvin.erik.mobibot.Utils.sendMessage
import org.pircbotx.hooks.types.GenericMessageEvent
class ChannelFeed(bot: Mobibot, channel: String) : AbstractCommand(bot) { 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"))
override val isOp = false override val isOpOnly = false
override val isPublic = true override val isPublic = true
override val isVisible = true override val isVisible = true
@ -53,22 +54,16 @@ class ChannelFeed(bot: Mobibot, channel: String) : AbstractCommand(bot) {
initProperties(FEED_PROP) initProperties(FEED_PROP)
} }
override fun commandResponse( override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
sender: String,
login: String,
args: String,
isOp: Boolean,
isPrivate: Boolean
) {
with(properties[FEED_PROP]) { with(properties[FEED_PROP]) {
if (!isNullOrBlank()) { if (!isNullOrBlank()) {
runBlocking { runBlocking {
launch { launch {
FeedReader(bot, sender, this@with).run() FeedReader(this@with, event).run()
} }
} }
} else { } else {
bot.send(sender, "There is no feed setup for this channel.", false) event.sendMessage("There is no feed setup for this channel.")
} }
} }
} }

View file

@ -32,35 +32,33 @@
package net.thauvin.erik.mobibot.commands package net.thauvin.erik.mobibot.commands
import net.thauvin.erik.mobibot.Mobibot import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import net.thauvin.erik.mobibot.Utils.bot
import net.thauvin.erik.mobibot.Utils.helpFormat import net.thauvin.erik.mobibot.Utils.helpFormat
import net.thauvin.erik.mobibot.Utils.isChannelOp
import org.pircbotx.hooks.types.GenericMessageEvent
class Cycle(bot: Mobibot) : AbstractCommand(bot) { class Cycle : AbstractCommand() {
@Suppress("MagicNumber")
private val wait = 10 private val wait = 10
override val name = "cycle" override val name = "cycle"
override val help = listOf("To have the bot leave the channel and come back:", helpFormat("%c $name")) override val help = listOf("To have the bot leave the channel and come back:", helpFormat("%c $name"))
override val isOp = true override val isOpOnly = true
override val isPublic = false override val isPublic = false
override val isVisible = true override val isVisible = true
override fun commandResponse( override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
sender: String, with(event.bot()) {
login: String, if (isChannelOp(channel, event)) {
args: String, runBlocking {
isOp: Boolean, sendIRC().message(channel, "${event.user.nick} asked me to leave. I'll be back!")
isPrivate: Boolean userChannelDao.getChannel(channel).send().part()
) { delay(wait * 1000L)
with(bot) { sendIRC().joinChannel(channel)
if (isOp) { }
send("$sender has just asked me to leave. I'll be back!")
sleep(wait)
partChannel(channel)
sleep(wait)
joinChannel(channel)
} else { } else {
helpDefault(sender, isOp, isPrivate) helpResponse(channel, args, event)
} }
} }
} }

View file

@ -1,59 +0,0 @@
/*
* Debug.kt
*
* Copyright (c) 2004-2021, Erik C. Thauvin (erik@thauvin.net)
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* Neither the name of this project nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.thauvin.erik.mobibot.commands
import net.thauvin.erik.mobibot.Constants
import net.thauvin.erik.mobibot.Mobibot
import org.apache.logging.log4j.Level
import org.apache.logging.log4j.core.config.Configurator
class Debug(bot: Mobibot) : AbstractCommand(bot) {
override val name = Constants.DEBUG_CMD
override val help = emptyList<String>()
override val isOp = true
override val isPublic = false
override val isVisible = false
override fun commandResponse(sender: String, login: String, args: String, isOp: Boolean, isPrivate: Boolean) {
if (isOp) {
with(bot) {
if (logger.isDebugEnabled) {
Configurator.setLevel(logger.name, loggerLevel)
} else {
Configurator.setLevel(logger.name, Level.DEBUG)
}
send(sender, "Debug logging is " + if (logger.isDebugEnabled) "enabled." else "disabled.", true)
}
}
}
}

View file

@ -32,25 +32,35 @@
package net.thauvin.erik.mobibot.commands package net.thauvin.erik.mobibot.commands
import net.thauvin.erik.mobibot.Mobibot import net.thauvin.erik.mobibot.Utils.bot
import net.thauvin.erik.mobibot.Utils.isChannelOp
import org.pircbotx.hooks.types.GenericMessageEvent
class Die(bot: Mobibot) : AbstractCommand(bot) { class Die : AbstractCommand() {
override val name = "die" override val name = "die"
override val help = emptyList<String>() override val help = emptyList<String>()
override val isOp = true override val isOpOnly = true
override val isPublic = false override val isPublic = false
override val isVisible = false override val isVisible = false
override fun commandResponse( override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
sender: String, with(event.bot()) {
login: String, if (isChannelOp(channel, event) && (properties[DIE_PROP].isNullOrBlank() || args == properties[DIE_PROP])) {
args: String, sendIRC().message(channel, "${event.user?.nick} has just signed my death sentence.")
isOp: Boolean, stopBotReconnect()
isPrivate: Boolean sendIRC().quitServer("The Bot is Out There!")
) {
if (isOp) {
bot.send("$sender has just signed my death sentence.")
bot.shutdown(sender)
} }
} }
} }
companion object {
/**
* Max days property.
*/
const val DIE_PROP = "die"
}
init {
initProperties(DIE_PROP)
}
}

View file

@ -32,13 +32,17 @@
package net.thauvin.erik.mobibot.commands package net.thauvin.erik.mobibot.commands
import net.thauvin.erik.mobibot.Mobibot
import net.thauvin.erik.mobibot.Utils.bold import net.thauvin.erik.mobibot.Utils.bold
import net.thauvin.erik.mobibot.Utils.bot
import net.thauvin.erik.mobibot.Utils.buildCmdSyntax import net.thauvin.erik.mobibot.Utils.buildCmdSyntax
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.sendList
import net.thauvin.erik.mobibot.Utils.sendMessage
import net.thauvin.erik.mobibot.commands.links.LinksMgr import net.thauvin.erik.mobibot.commands.links.LinksMgr
import org.pircbotx.hooks.types.GenericMessageEvent
class Ignore(bot: Mobibot) : AbstractCommand(bot) { class Ignore : AbstractCommand() {
private val me = "me" private val me = "me"
init { init {
@ -58,7 +62,7 @@ class Ignore(bot: Mobibot) : AbstractCommand(bot) {
arrayOf("To add/remove nicks from the ignored list:", helpFormat("%c $name <nick> [<nick> ...]")) arrayOf("To add/remove nicks from the ignored list:", helpFormat("%c $name <nick> [<nick> ...]"))
) )
override val isOp = false override val isOpOnly = false
override val isPublic = true override val isPublic = true
override val isVisible = true override val isVisible = true
@ -73,56 +77,45 @@ class Ignore(bot: Mobibot) : AbstractCommand(bot) {
} }
} }
override fun commandResponse( override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
sender: String,
login: String,
args: String,
isOp: Boolean,
isPrivate: Boolean
) {
val isMe = args.trim().equals(me, true) val isMe = args.trim().equals(me, true)
if (isMe || !isOp) { if (isMe || !isChannelOp(channel, event)) {
val nick = sender.lowercase() val nick = event.user.nick.lowercase()
ignoreNick(bot, nick, isMe, isPrivate) ignoreNick(nick, isMe, event)
} else { } else {
ignoreOp(bot, sender, args, isPrivate) ignoreOp(args, event)
} }
} }
override fun helpResponse( override fun helpResponse(channel: String, topic: String, event: GenericMessageEvent): Boolean {
command: String, return if (isChannelOp(channel, event)) {
sender: String,
isOp: Boolean,
isPrivate: Boolean
): Boolean {
return if (isOp) {
for (h in helpOp) { for (h in helpOp) {
bot.send(sender, buildCmdSyntax(h, bot.nick, isPrivate), isPrivate) event.sendMessage(buildCmdSyntax(h, event.bot().nick, true))
} }
true true
} else { } else {
super.helpResponse(command, sender, isOp, isPrivate) super.helpResponse(channel, topic, event)
} }
} }
private fun ignoreNick(bot: Mobibot, sender: String, isMe: Boolean, isPrivate: Boolean) { private fun ignoreNick(sender: String, isMe: Boolean, event: GenericMessageEvent) {
if (isMe) { if (isMe) {
if (ignored.remove(sender)) { if (ignored.remove(sender)) {
bot.send(sender, "You are no longer ignored.", isPrivate) event.sendMessage("You are no longer ignored.")
} else { } else {
ignored.add(sender) ignored.add(sender)
bot.send(sender, "You are now ignored.", isPrivate) event.sendMessage("You are now ignored.")
} }
} else { } else {
if (ignored.contains(sender)) { if (ignored.contains(sender)) {
bot.send(sender, "You are currently ignored.", isPrivate) event.sendMessage("You are currently ignored.")
} else { } else {
bot.send(sender, "You are not currently ignored.", isPrivate) event.sendMessage("You are not currently ignored.")
} }
} }
} }
private fun ignoreOp(bot: Mobibot, sender: String, args: String, isPrivate: Boolean) { private fun ignoreOp(args: String, event: GenericMessageEvent) {
if (args.isNotEmpty()) { if (args.isNotEmpty()) {
val nicks = args.lowercase().split(" ") val nicks = args.lowercase().split(" ")
for (nick in nicks) { for (nick in nicks) {
@ -138,11 +131,10 @@ class Ignore(bot: Mobibot) : AbstractCommand(bot) {
} }
if (ignored.size > 0) { if (ignored.size > 0) {
bot.send(sender, "The following nicks are ignored:", isPrivate) event.sendMessage("The following nicks are ignored:")
@Suppress("MagicNumber") event.sendList(ignored.sorted(), 8, isIndent = true)
bot.sendList(sender, ignored.sorted(), 8, isPrivate = isPrivate, isIndent = true)
} else { } else {
bot.send(sender, "No one is currently ${bold("ignored")}.", isPrivate) event.sendMessage("No one is currently ${bold("ignored")}.")
} }
} }

View file

@ -31,50 +31,46 @@
*/ */
package net.thauvin.erik.mobibot.commands package net.thauvin.erik.mobibot.commands
import net.thauvin.erik.mobibot.Mobibot
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.capitalise
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.sendList
import net.thauvin.erik.mobibot.Utils.sendMessage
import net.thauvin.erik.mobibot.Utils.uptime import net.thauvin.erik.mobibot.Utils.uptime
import net.thauvin.erik.mobibot.commands.links.LinksMgr import net.thauvin.erik.mobibot.commands.links.LinksMgr
import net.thauvin.erik.mobibot.commands.tell.Tell
import org.pircbotx.hooks.types.GenericMessageEvent
import java.lang.management.ManagementFactory import java.lang.management.ManagementFactory
class Info(bot: Mobibot?) : AbstractCommand(bot!!) { class Info(private val tell: Tell) : AbstractCommand() {
private val allVersions = listOf( private val allVersions = listOf(
"${ReleaseInfo.PROJECT.capitalise()} ${ReleaseInfo.VERSION} (${green(ReleaseInfo.WEBSITE)})", "${ReleaseInfo.PROJECT.capitalise()} ${ReleaseInfo.VERSION} (${green(ReleaseInfo.WEBSITE)})",
"Written by ${ReleaseInfo.AUTHOR} (${green(ReleaseInfo.AUTHOR_URL)})" "Written by ${ReleaseInfo.AUTHOR} (${green(ReleaseInfo.AUTHOR_URL)})"
) )
override val name = "info" override val name = "info"
override val help = listOf("To view information about the bot:", helpFormat("%c $name")) override val help = listOf("To view information about the bot:", helpFormat("%c $name"))
override val isOp = false override val isOpOnly = false
override val isPublic = true override val isPublic = true
override val isVisible = true override val isVisible = true
override fun commandResponse( override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
sender: String, event.sendList(allVersions, 1)
login: String,
args: String,
isOp: Boolean,
isPrivate: Boolean
) {
with(bot) {
sendList(sender, allVersions, 1, isPrivate = isPrivate)
val info = StringBuilder() val info = StringBuilder()
info.append("Uptime: ") info.append("Uptime: ")
.append(uptime(ManagementFactory.getRuntimeMXBean().uptime)) .append(uptime(ManagementFactory.getRuntimeMXBean().uptime))
.append(" [Entries: ") .append(" [Entries: ")
.append(LinksMgr.entries.size) .append(LinksMgr.entries.links.size)
if (isOp) { if (isChannelOp(channel, event)) {
if (tell.isEnabled()) { if (tell.isEnabled()) {
info.append(", Messages: ").append(tell.size()) info.append(", Messages: ").append(tell.size())
} }
if (twitter.isAutoPost) { if (LinksMgr.twitter.isAutoPost) {
info.append(", Twitter: ").append(twitter.entriesCount()) info.append(", Twitter: ").append(LinksMgr.twitter.entriesCount())
} }
} }
info.append(", Recap: ").append(Recap.recaps.size).append(']') info.append(", Recap: ").append(Recap.recaps.size).append(']')
send(sender, info.toString(), isPrivate) event.sendMessage(info.toString())
}
} }
} }

View file

@ -32,27 +32,21 @@
package net.thauvin.erik.mobibot.commands package net.thauvin.erik.mobibot.commands
import net.thauvin.erik.mobibot.Mobibot import net.thauvin.erik.mobibot.Utils.bot
import net.thauvin.erik.mobibot.Utils.helpFormat import net.thauvin.erik.mobibot.Utils.helpFormat
import net.thauvin.erik.mobibot.Utils.isChannelOp
import org.pircbotx.hooks.types.GenericMessageEvent
class Me(bot: Mobibot) : AbstractCommand(bot) { 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>"))
override val isOp = true override val isOpOnly = true
override val isPublic = false override val isPublic = false
override val isVisible = true override val isVisible = true
override fun commandResponse( override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
sender: String, if (isChannelOp(channel, event)) {
login: String, event.bot().sendIRC().action(channel, args)
args: String,
isOp: Boolean,
isPrivate: Boolean
) {
if (isOp) {
bot.action(args)
} else {
bot.helpDefault(sender, isOp, isPrivate)
} }
} }
} }

View file

@ -32,35 +32,29 @@
package net.thauvin.erik.mobibot.commands package net.thauvin.erik.mobibot.commands
import net.thauvin.erik.mobibot.Mobibot
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.sendList
import org.pircbotx.hooks.types.GenericMessageEvent
class Modules(bot: Mobibot) : AbstractCommand(bot) { class Modules(private val modulesNames: List<String>) : AbstractCommand() {
override val name = "modules" override val name = "modules"
override val help = listOf("To view a list of enabled modules:", helpFormat("%c $name")) override val help = listOf("To view a list of enabled modules:", helpFormat("%c $name"))
override val isOp = true override val isOpOnly = true
override val isPublic = false override val isPublic = false
override val isVisible = true override val isVisible = true
override fun commandResponse( override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
sender: String, if (isChannelOp(channel, event)) {
login: String,
args: String,
isOp: Boolean,
isPrivate: Boolean
) {
with(bot) {
if (isOp) {
if (modulesNames.isEmpty()) { if (modulesNames.isEmpty()) {
send(sender, "There are no enabled modules.", isPrivate) event.respondPrivateMessage("There are no enabled modules.")
} else { } else {
send(sender, "The enabled modules are: ", isPrivate) event.respondPrivateMessage("The enabled modules are: ")
@Suppress("MagicNumber") event.sendList(modulesNames, 7, isIndent = true)
sendList(sender, modulesNames, 7, isPrivate = isPrivate, isIndent = true)
} }
} else { } else {
helpDefault(sender, isOp, isPrivate) helpResponse(channel, args, event)
}
} }
} }
} }

View file

@ -32,35 +32,30 @@
package net.thauvin.erik.mobibot.commands package net.thauvin.erik.mobibot.commands
import net.thauvin.erik.mobibot.Mobibot import net.thauvin.erik.mobibot.Utils.bot
import net.thauvin.erik.mobibot.Utils.helpFormat import net.thauvin.erik.mobibot.Utils.helpFormat
import net.thauvin.erik.mobibot.Utils.isChannelOp
import org.pircbotx.hooks.types.GenericMessageEvent
class Msg(bot: Mobibot) : AbstractCommand(bot) { class Msg : AbstractCommand() {
override val name = "msg" override val name = "msg"
override val help = listOf( override val help = listOf(
"To have the bot send a private message to someone:", "To have the bot send a private message to someone:",
helpFormat("%c $name <nick> <text>") helpFormat("%c $name <nick> <text>")
) )
override val isOp = true override val isOpOnly = true
override val isPublic = true override val isPublic = false
override val isVisible = true override val isVisible = true
override fun commandResponse( override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
sender: String, if (isChannelOp(channel, event)) {
login: String,
args: String,
isOp: Boolean,
isPrivate: Boolean
) {
if (isOp) {
val msg = args.split(" ", limit = 2) val msg = args.split(" ", limit = 2)
if (args.length > 2) { if (args.length > 2) {
bot.send(msg[0], msg[1], isPrivate) event.bot().sendIRC().message(msg[0], msg[1])
event.respondPrivateMessage("A message was sent to ${msg[0]}")
} else { } else {
helpResponse(name, sender, isOp, isPrivate) helpResponse(channel, args, event)
} }
} else {
bot.helpDefault(sender, isOp, isPrivate)
} }
} }
} }

View file

@ -32,27 +32,21 @@
package net.thauvin.erik.mobibot.commands package net.thauvin.erik.mobibot.commands
import net.thauvin.erik.mobibot.Mobibot import net.thauvin.erik.mobibot.Utils.bot
import net.thauvin.erik.mobibot.Utils.helpFormat import net.thauvin.erik.mobibot.Utils.helpFormat
import net.thauvin.erik.mobibot.Utils.isChannelOp
import org.pircbotx.hooks.types.GenericMessageEvent
class Nick(bot: Mobibot) : AbstractCommand(bot) { 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>"))
override val isOp = true override val isOpOnly = true
override val isPublic = true override val isPublic = true
override val isVisible = true override val isVisible = true
override fun commandResponse( override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
sender: String, if (isChannelOp(channel, event)) {
login: String, event.bot().sendIRC().changeNick(args)
args: String,
isOp: Boolean,
isPrivate: Boolean
) {
if (isOp) {
bot.changeNick(args)
} else {
bot.helpDefault(sender, isOp, isPrivate)
} }
} }
} }

View file

@ -32,19 +32,20 @@
package net.thauvin.erik.mobibot.commands package net.thauvin.erik.mobibot.commands
import net.thauvin.erik.mobibot.Mobibot
import net.thauvin.erik.mobibot.Utils.helpFormat import net.thauvin.erik.mobibot.Utils.helpFormat
import net.thauvin.erik.mobibot.Utils.sendMessage
import net.thauvin.erik.mobibot.Utils.toUtcDateTime import net.thauvin.erik.mobibot.Utils.toUtcDateTime
import org.pircbotx.hooks.types.GenericMessageEvent
import java.time.Clock import java.time.Clock
import java.time.LocalDateTime import java.time.LocalDateTime
class Recap(bot: Mobibot) : AbstractCommand(bot) { class Recap : AbstractCommand() {
override val name = "recap" override val name = "recap"
override val help = listOf( override val help = listOf(
"To list the last 10 public channel messages:", "To list the last 10 public channel messages:",
helpFormat("%c $name") helpFormat("%c $name")
) )
override val isOp = false override val isOpOnly = false
override val isPublic = true override val isPublic = true
override val isVisible = true override val isVisible = true
@ -61,26 +62,19 @@ class Recap(bot: Mobibot) : AbstractCommand(bot) {
LocalDateTime.now(Clock.systemUTC()).toUtcDateTime() LocalDateTime.now(Clock.systemUTC()).toUtcDateTime()
+ " - $sender" + (if (isAction) " " else ": ") + message + " - $sender" + (if (isAction) " " else ": ") + message
) )
@Suppress("MagicNumber")
if (recaps.size > 10) { if (recaps.size > 10) {
recaps.removeFirst() recaps.removeFirst()
} }
} }
} }
override fun commandResponse( override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
sender: String,
login: String,
args: String,
isOp: Boolean,
isPrivate: Boolean
) {
if (recaps.isNotEmpty()) { if (recaps.isNotEmpty()) {
for (r in recaps) { for (r in recaps) {
bot.send(sender, r, isPrivate) event.sendMessage(r)
} }
} else { } else {
bot.send(sender, "Sorry, nothing to recap.", isPrivate) event.sendMessage("Sorry, nothing to recap.")
} }
} }
} }

View file

@ -32,28 +32,22 @@
package net.thauvin.erik.mobibot.commands package net.thauvin.erik.mobibot.commands
import net.thauvin.erik.mobibot.Mobibot import net.thauvin.erik.mobibot.Utils.bot
import net.thauvin.erik.mobibot.Utils.helpFormat import net.thauvin.erik.mobibot.Utils.helpFormat
import net.thauvin.erik.mobibot.Utils.isChannelOp
import org.pircbotx.hooks.types.GenericMessageEvent
class Say(bot: Mobibot) : AbstractCommand(bot) { 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>"))
override val isOp = true override val isOpOnly = true
override val isPublic = false override val isPublic = false
override val isVisible = true override val isVisible = true
override fun commandResponse( override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
sender: String, if (isChannelOp(channel, event)) {
login: String, event.bot().sendIRC().message(channel, args)
args: String,
isOp: Boolean,
isPrivate: Boolean
) {
if (isOp) {
bot.send(args)
} else {
bot.helpDefault(sender, isOp, isPrivate)
} }
} }
} }

View file

@ -32,36 +32,29 @@
package net.thauvin.erik.mobibot.commands package net.thauvin.erik.mobibot.commands
import net.thauvin.erik.mobibot.Mobibot import net.thauvin.erik.mobibot.Utils.bot
import net.thauvin.erik.mobibot.Utils.helpFormat import net.thauvin.erik.mobibot.Utils.helpFormat
import net.thauvin.erik.mobibot.Utils.sendList
import org.pircbotx.hooks.types.GenericMessageEvent
class Users(bot: Mobibot) : AbstractCommand(bot) { 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"))
override val isOp = false override val isOpOnly = false
override val isPublic = true override val isPublic = true
override val isVisible = true override val isVisible = true
override fun commandResponse( override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
sender: String,
login: String,
args: String,
isOp: Boolean,
isPrivate: Boolean
) {
val nicks = mutableListOf<String>() val nicks = mutableListOf<String>()
with(bot) { val ch = event.bot().userChannelDao.getChannel(channel)
getUsers(channel).forEach { user -> ch.users.forEach {
if (isOp(user.nick)) { if (it.channelsOpIn.contains(ch)) {
nicks.add("@${user.nick}") nicks.add("@${it.nick}")
} else { } else {
nicks.add(user.nick) nicks.add(it.nick)
} }
} }
event.sendList(nicks, 8)
@Suppress("MagicNumber")
sendList(sender, nicks.sorted(), 8, isPrivate = isPrivate, isIndent = true)
}
} }
} }

View file

@ -31,12 +31,14 @@
*/ */
package net.thauvin.erik.mobibot.commands package net.thauvin.erik.mobibot.commands
import net.thauvin.erik.mobibot.Mobibot
import net.thauvin.erik.mobibot.ReleaseInfo import net.thauvin.erik.mobibot.ReleaseInfo
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.sendList
import net.thauvin.erik.mobibot.Utils.toIsoLocalDate import net.thauvin.erik.mobibot.Utils.toIsoLocalDate
import org.pircbotx.hooks.types.GenericMessageEvent
class Versions(bot: Mobibot) : AbstractCommand(bot) { class Versions : AbstractCommand() {
private val allVersions = listOf( private val allVersions = listOf(
"Version: ${ReleaseInfo.VERSION} (${ReleaseInfo.BUILDDATE.toIsoLocalDate()})", "Version: ${ReleaseInfo.VERSION} (${ReleaseInfo.BUILDDATE.toIsoLocalDate()})",
"Platform: " + System.getProperty("os.name") + ' ' + System.getProperty("os.version") "Platform: " + System.getProperty("os.name") + ' ' + System.getProperty("os.version")
@ -45,21 +47,13 @@ class Versions(bot: Mobibot) : AbstractCommand(bot) {
) )
override val name = "versions" override val name = "versions"
override val help = listOf("To view the versions data (bot, platform, java, etc.):", helpFormat("%c $name")) override val help = listOf("To view the versions data (bot, platform, java, etc.):", helpFormat("%c $name"))
override val isOp = true override val isOpOnly = true
override val isPublic = false override val isPublic = false
override val isVisible = true override val isVisible = true
override fun commandResponse( override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
sender: String, if (isChannelOp(channel, event)) {
login: String, event.sendList(allVersions, 1)
args: String,
isOp: Boolean,
isPrivate: Boolean
) {
if (isOp) {
bot.sendList(sender, allVersions, 1, isPrivate = isPrivate)
} else {
bot.helpDefault(sender, false, isPrivate)
} }
} }
} }

View file

@ -33,14 +33,16 @@
package net.thauvin.erik.mobibot.commands.links package net.thauvin.erik.mobibot.commands.links
import net.thauvin.erik.mobibot.Constants import net.thauvin.erik.mobibot.Constants
import net.thauvin.erik.mobibot.Mobibot
import net.thauvin.erik.mobibot.Utils.bold import net.thauvin.erik.mobibot.Utils.bold
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.sendMessage
import net.thauvin.erik.mobibot.commands.AbstractCommand import net.thauvin.erik.mobibot.commands.AbstractCommand
import net.thauvin.erik.mobibot.entries.EntriesUtils 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
class Comment(bot: Mobibot) : AbstractCommand(bot) { class Comment : AbstractCommand() {
override val name = COMMAND override val name = COMMAND
override val help = listOf( override val help = listOf(
"To add a comment:", "To add a comment:",
@ -51,7 +53,7 @@ class Comment(bot: Mobibot) : AbstractCommand(bot) {
"To delete a comment, use its label and a minus sign: ", "To delete a comment, use its label and a minus sign: ",
helpFormat("${Constants.LINK_CMD}1.1:-") helpFormat("${Constants.LINK_CMD}1.1:-")
) )
override val isOp = false override val isOpOnly = false
override val isPublic = true override val isPublic = true
override val isVisible = true override val isVisible = true
@ -59,29 +61,22 @@ class Comment(bot: Mobibot) : AbstractCommand(bot) {
const val COMMAND = "comment" const val COMMAND = "comment"
} }
override fun commandResponse( override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
sender: String,
login: String,
args: String,
isOp: Boolean,
isPrivate: Boolean
) {
@Suppress("MagicNumber")
val cmds = args.substring(1).split("[.:]".toRegex(), 3) val cmds = args.substring(1).split("[.:]".toRegex(), 3)
val index = cmds[0].toInt() - 1 val entryIndex = cmds[0].toInt() - 1
if (index < LinksMgr.entries.size) { if (entryIndex < LinksMgr.entries.links.size && LinksMgr.isUpToDate(event)) {
val entry: EntryLink = LinksMgr.entries[index] val entry: EntryLink = LinksMgr.entries.links[entryIndex]
val commentIndex = cmds[1].toInt() - 1 val commentIndex = cmds[1].toInt() - 1
if (commentIndex < entry.comments.size) { if (commentIndex < entry.comments.size) {
when (val cmd = cmds[2].trim()) { when (val cmd = cmds[2].trim()) {
"" -> showComment(bot, entry, index, commentIndex) // L1.1: "" -> showComment(entry, entryIndex, commentIndex, event) // L1.1:
"-" -> deleteComment(bot, sender, isOp, entry, index, commentIndex) // L11:- "-" -> deleteComment(channel, entry, entryIndex, commentIndex, event) // L1.1:-
else -> { else -> {
if (cmd.startsWith('?')) { // L1.1:?<author> if (cmd.startsWith('?')) { // L1.1:?<author>
changeAuthor(bot, cmd, sender, isOp, entry, index, commentIndex) changeAuthor(channel, cmd, entry, entryIndex, commentIndex, event)
} else { // L1.1:<comment> } else { // L1.1:<comment>
setComment(bot, cmd, sender, entry, index, commentIndex) setComment(cmd, entry, entryIndex, commentIndex, event)
} }
} }
} }
@ -89,20 +84,11 @@ class Comment(bot: Mobibot) : AbstractCommand(bot) {
} }
} }
override fun helpResponse( override fun helpResponse(channel: String, topic: String, event: GenericMessageEvent): Boolean {
command: String, if (super.helpResponse(channel, topic, event)) {
sender: String, if (isChannelOp(channel, event)) {
isOp: Boolean, event.sendMessage("To change a comment's author:")
isPrivate: Boolean event.sendMessage(helpFormat("${Constants.LINK_CMD}1.1:?<nick>"))
): Boolean {
if (super.helpResponse(command, sender, isOp, isPrivate)) {
if (isOp) {
bot.send(sender, "To change a comment's author:", isPrivate)
bot.send(
sender,
helpFormat("${Constants.LINK_CMD}1.1:?<nick>"),
isPrivate
)
} }
return true return true
} }
@ -114,49 +100,52 @@ class Comment(bot: Mobibot) : AbstractCommand(bot) {
} }
private fun changeAuthor( private fun changeAuthor(
bot: Mobibot, channel: String,
cmd: String, cmd: String,
sender: String,
isOp: Boolean,
entry: EntryLink, entry: EntryLink,
index: Int, entryIndex: Int,
commentIndex: Int commentIndex: Int,
event: GenericMessageEvent
) { ) {
if (isOp && cmd.length > 1) { if (isChannelOp(channel, event) && cmd.length > 1) {
val comment = entry.getComment(commentIndex) val comment = entry.getComment(commentIndex)
comment.nick = cmd.substring(1) comment.nick = cmd.substring(1)
bot.send(EntriesUtils.buildComment(index, commentIndex, comment)) event.sendMessage(EntriesUtils.buildComment(entryIndex, commentIndex, comment))
LinksMgr.saveEntries(bot, false) LinksMgr.entries.save()
} else { } else {
bot.send(sender, "Please ask a channel op to change the author of this comment for you.", false) event.sendMessage("Please ask a channel op to change the author of this comment for you.")
} }
} }
private fun deleteComment( private fun deleteComment(
bot: Mobibot, channel: String,
sender: String,
isOp: Boolean,
entry: EntryLink, entry: EntryLink,
index: Int, entryIndex: Int,
commentIndex: Int commentIndex: Int,
event: GenericMessageEvent
) { ) {
if (isOp || sender == entry.getComment(commentIndex).nick) { if (isChannelOp(channel, event) || event.user.nick == entry.getComment(commentIndex).nick) {
entry.deleteComment(commentIndex) entry.deleteComment(commentIndex)
bot.send("Comment ${EntriesUtils.buildLinkCmd(index)}.${commentIndex + 1} removed.") event.sendMessage("Comment ${EntriesUtils.buildLinkLabel(entryIndex)}.${commentIndex + 1} removed.")
LinksMgr.saveEntries(bot, false) LinksMgr.entries.save()
} else { } else {
bot.send(sender, "Please ask a channel op to delete this comment for you.", false) event.sendMessage("Please ask a channel op to delete this comment for you.")
} }
} }
private fun setComment(bot: Mobibot, cmd: String, sender: String, entry: EntryLink, index: Int, commentIndex: Int) { private fun setComment(
entry.setComment(commentIndex, cmd, sender) cmd: String,
val comment = entry.getComment(commentIndex) entry: EntryLink,
bot.send(sender, EntriesUtils.buildComment(index, commentIndex, comment), false) entryIndex: Int,
LinksMgr.saveEntries(bot, false) commentIndex: Int,
event: GenericMessageEvent
) {
entry.setComment(commentIndex, cmd, event.user.nick)
event.sendMessage(EntriesUtils.buildComment(entryIndex, commentIndex, entry.getComment(commentIndex)))
LinksMgr.entries.save()
} }
private fun showComment(bot: Mobibot, entry: EntryLink, index: Int, commentIndex: Int) { private fun showComment(entry: EntryLink, entryIndex: Int, commentIndex: Int, event: GenericMessageEvent) {
bot.send(EntriesUtils.buildComment(index, commentIndex, entry.getComment(commentIndex))) event.sendMessage(EntriesUtils.buildComment(entryIndex, commentIndex, entry.getComment(commentIndex)))
} }
} }

View file

@ -33,25 +33,29 @@
package net.thauvin.erik.mobibot.commands.links package net.thauvin.erik.mobibot.commands.links
import net.thauvin.erik.mobibot.Constants import net.thauvin.erik.mobibot.Constants
import net.thauvin.erik.mobibot.Mobibot import net.thauvin.erik.mobibot.Pinboard
import net.thauvin.erik.mobibot.Utils.bold import net.thauvin.erik.mobibot.Utils.bold
import net.thauvin.erik.mobibot.Utils.bot
import net.thauvin.erik.mobibot.Utils.helpFormat import net.thauvin.erik.mobibot.Utils.helpFormat
import net.thauvin.erik.mobibot.Utils.sendMessage
import net.thauvin.erik.mobibot.Utils.today import net.thauvin.erik.mobibot.Utils.today
import net.thauvin.erik.mobibot.commands.AbstractCommand import net.thauvin.erik.mobibot.commands.AbstractCommand
import net.thauvin.erik.mobibot.commands.Ignore import net.thauvin.erik.mobibot.commands.Ignore.Companion.isNotIgnored
import net.thauvin.erik.mobibot.entries.EntriesMgr import net.thauvin.erik.mobibot.entries.Entries
import net.thauvin.erik.mobibot.entries.EntriesUtils import net.thauvin.erik.mobibot.entries.EntriesUtils
import net.thauvin.erik.mobibot.entries.EntryLink import net.thauvin.erik.mobibot.entries.EntryLink
import net.thauvin.erik.mobibot.modules.Twitter
import org.jsoup.Jsoup import org.jsoup.Jsoup
import org.pircbotx.hooks.types.GenericMessageEvent
import java.io.IOException import java.io.IOException
class LinksMgr(bot: Mobibot) : AbstractCommand(bot) { class LinksMgr : AbstractCommand() {
private val keywords: MutableList<String> = mutableListOf()
private val defaultTags: MutableList<String> = mutableListOf() private val defaultTags: MutableList<String> = mutableListOf()
private val keywords: MutableList<String> = mutableListOf()
override val name = Constants.LINK_CMD override val name = Constants.LINK_CMD
override val help = emptyList<String>() override val help = emptyList<String>()
override val isOp = false override val isOpOnly = false
override val isPublic = false override val isPublic = false
override val isVisible = false override val isVisible = false
@ -65,52 +69,35 @@ class LinksMgr(bot: Mobibot) : AbstractCommand(bot) {
const val TAGS_PROP = "tags" const val TAGS_PROP = "tags"
const val TAG_MATCH = ", *| +" const val TAG_MATCH = ", *| +"
// Entries array /** Entries array **/
@JvmField val entries = Entries()
val entries = mutableListOf<EntryLink>()
// History/backlogs array /** Pinboard handler. **/
@JvmField val pinboard = Pinboard()
val history = mutableListOf<String>()
/** Twitter handler. **/
val twitter = Twitter()
/** Let the user know if the entries are too old to be modified. **/
@JvmStatic @JvmStatic
var startDate: String = today() fun isUpToDate(event: GenericMessageEvent): Boolean {
private set if (entries.lastPubDate != today()) {
event.sendMessage("The links are too old to be updated.")
/** return false
* Saves the entries.
*
* @param isDayBackup Set the `true` if the daily backup file should also be created.
*/
@JvmStatic
fun saveEntries(bot: Mobibot, isDayBackup: Boolean) {
EntriesMgr.saveEntries(bot, entries, history, isDayBackup)
} }
return true
@JvmStatic
fun startup(current: String, backlogs: String, channel: String) {
startDate = EntriesMgr.loadEntries(current, channel, entries)
if (today() != startDate) {
entries.clear()
startDate = today()
}
EntriesMgr.loadBacklogs(backlogs, history)
} }
} }
override fun commandResponse( override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
sender: String,
login: String,
args: String,
isOp: Boolean,
isPrivate: Boolean
) {
val cmds = args.split(" ".toRegex(), 2) val cmds = args.split(" ".toRegex(), 2)
val sender = event.user.nick
val botNick = event.bot().nick
val login = event.user.login
if (Ignore.isNotIgnored(sender) && (cmds.size == 1 || !cmds[1].contains(bot.nick))) { if (isNotIgnored(sender) && (cmds.size == 1 || !cmds[1].contains(botNick))) {
val link = cmds[0].trim() val link = cmds[0].trim()
if (!isDupEntry(bot, sender, link, isPrivate)) { if (!isDupEntry(link, event)) {
val isBackup = saveDayBackup(bot)
var title = "" var title = ""
val tags = ArrayList<String>(defaultTags) val tags = ArrayList<String>(defaultTags)
if (cmds.size == 2) { if (cmds.size == 2) {
@ -129,37 +116,32 @@ class LinksMgr(bot: Mobibot) : AbstractCommand(bot) {
matchTagKeywords(title, tags) matchTagKeywords(title, tags)
} }
entries.add(EntryLink(link, title, sender, login, bot.channel, tags)) // Links are old, clear them
val index: Int = entries.size - 1 if (entries.lastPubDate != today()) {
val entry: EntryLink = entries[index] entries.links.clear()
bot.send(EntriesUtils.buildLink(index, entry)) }
// Add Entry to pinboard. val entry = EntryLink(link, title, sender, login, channel, tags)
bot.addPin(entry) entries.links.add(entry)
val index = entries.links.lastIndexOf(entry)
event.sendMessage(EntriesUtils.buildLink(index, entry))
pinboard.addPin(event.bot().serverHostname, entry)
// Queue link for posting to Twitter. // Queue link for posting to Twitter.
bot.twitter.queueEntry(index) twitter.queueEntry(index)
saveEntries(bot, isBackup) entries.save()
if (Constants.NO_TITLE == entry.title) { if (Constants.NO_TITLE == entry.title) {
bot.send(sender, "Please specify a title, by typing:", isPrivate) event.sendMessage("Please specify a title, by typing:")
bot.send( event.sendMessage(helpFormat("${EntriesUtils.buildLinkLabel(index)}:|This is the title"))
sender,
helpFormat("${EntriesUtils.buildLinkCmd(index)}:|This is the title"),
isPrivate
)
} }
} }
} }
} }
override fun helpResponse( override fun helpResponse(channel: String, topic: String, event: GenericMessageEvent): Boolean = false
command: String,
sender: String,
isOp: Boolean,
isPrivate: Boolean
): Boolean = false
override fun matches(message: String): Boolean { override fun matches(message: String): Boolean {
return message.matches(LINK_MATCH.toRegex()) return message.matches(LINK_MATCH.toRegex())
@ -180,12 +162,12 @@ class LinksMgr(bot: Mobibot) : AbstractCommand(bot) {
return Constants.NO_TITLE return Constants.NO_TITLE
} }
private fun isDupEntry(bot: Mobibot, sender: String, link: String, isPrivate: Boolean): Boolean { private fun isDupEntry(link: String, event: GenericMessageEvent): Boolean {
synchronized(entries) { synchronized(entries) {
for (i in entries.indices) { for (i in entries.links.indices) {
if (link == entries[i].link) { if (link == entries.links[i].link) {
val entry: EntryLink = entries[i] val entry: EntryLink = entries.links[i]
bot.send(sender, bold("Duplicate") + " >> " + EntriesUtils.buildLink(i, entry), isPrivate) event.sendMessage(bold("Duplicate") + " >> " + EntriesUtils.buildLink(i, entry))
return true return true
} }
} }
@ -202,17 +184,6 @@ class LinksMgr(bot: Mobibot) : AbstractCommand(bot) {
} }
} }
private fun saveDayBackup(bot: Mobibot): Boolean {
if (today() != startDate) {
saveEntries(bot, true)
entries.clear()
startDate = today()
return true
}
return false
}
override fun setProperty(key: String, value: String) { override fun setProperty(key: String, value: String) {
super.setProperty(key, value) super.setProperty(key, value)
if (KEYWORDS_PROP == key) { if (KEYWORDS_PROP == key) {

View file

@ -33,15 +33,18 @@
package net.thauvin.erik.mobibot.commands.links package net.thauvin.erik.mobibot.commands.links
import net.thauvin.erik.mobibot.Constants import net.thauvin.erik.mobibot.Constants
import net.thauvin.erik.mobibot.Mobibot
import net.thauvin.erik.mobibot.Utils.bold import net.thauvin.erik.mobibot.Utils.bold
import net.thauvin.erik.mobibot.Utils.bot
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.sendMessage
import net.thauvin.erik.mobibot.commands.AbstractCommand import net.thauvin.erik.mobibot.commands.AbstractCommand
import net.thauvin.erik.mobibot.commands.links.LinksMgr.Companion.entries import net.thauvin.erik.mobibot.commands.links.LinksMgr.Companion.entries
import net.thauvin.erik.mobibot.entries.EntriesUtils 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
class Posting(bot: Mobibot) : AbstractCommand(bot) { class Posting : AbstractCommand() {
override val name = "posting" override val name = "posting"
override val help = listOf( override val help = listOf(
"Post a URL, by saying it on a line on its own:", "Post a URL, by saying it on a line on its own:",
@ -55,30 +58,27 @@ class Posting(bot: Mobibot) : AbstractCommand(bot) {
"To edit a comment, see: ", "To edit a comment, see: ",
helpFormat("%c ${Constants.HELP_CMD} ${Comment.COMMAND}") helpFormat("%c ${Constants.HELP_CMD} ${Comment.COMMAND}")
) )
override val isOp = false override val isOpOnly = false
override val isPublic = true override val isPublic = true
override val isVisible = true override val isVisible = true
override fun commandResponse( override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
sender: String,
login: String,
args: String,
isOp: Boolean,
isPrivate: Boolean
) {
val cmds = args.substring(1).split(":", limit = 2) val cmds = args.substring(1).split(":", limit = 2)
val index = cmds[0].toInt() - 1 val entryIndex = cmds[0].toInt() - 1
if (index < entries.size) { if (entryIndex < entries.links.size) {
when (val cmd = cmds[1].trim()) { val cmd = cmds[1].trim()
"" -> showEntry(index) if (cmd.isBlank()) {
"-" -> removeEntry(sender, login, isOp, index) // L1:- showEntry(entryIndex, event) // L1:
else -> { } else if (LinksMgr.isUpToDate(event)) {
if (cmd == "-") {
removeEntry(channel, entryIndex, event) // L1:-
} else {
when (cmd[0]) { when (cmd[0]) {
'|' -> changeTitle(cmd, index) // L1:|<title> '|' -> changeTitle(cmd, entryIndex, event) // L1:|<title>
'=' -> changeUrl(cmd, login, isOp, index) // L1:=<url> '=' -> changeUrl(channel, cmd, entryIndex, event) // L1:=<url>
'?' -> changeAuthor(cmd, sender, isOp, index) // L1:?<author> '?' -> changeAuthor(channel, cmd, entryIndex, event) // L1:?<author>
else -> addComment(cmd, sender, index) // L1:<comment> else -> addComment(cmd, entryIndex, event) // L1:<comment>
} }
} }
} }
@ -89,73 +89,75 @@ class Posting(bot: Mobibot) : AbstractCommand(bot) {
return message.matches("${Constants.LINK_CMD}[0-9]+:.*".toRegex()) return message.matches("${Constants.LINK_CMD}[0-9]+:.*".toRegex())
} }
private fun addComment(cmd: String, sender: String, index: Int) { private fun addComment(cmd: String, entryIndex: Int, event: GenericMessageEvent) {
val entry: EntryLink = entries[index] val entry: EntryLink = entries.links[entryIndex]
val commentIndex = entry.addComment(cmd, sender) val commentIndex = entry.addComment(cmd, event.user.nick)
val comment = entry.getComment(commentIndex) val comment = entry.getComment(commentIndex)
bot.send(sender, EntriesUtils.buildComment(index, commentIndex, comment), false) event.sendMessage(EntriesUtils.buildComment(entryIndex, commentIndex, comment))
LinksMgr.saveEntries(bot, false) entries.save()
} }
private fun changeTitle(cmd: String, index: Int) { private fun changeTitle(cmd: String, entryIndex: Int, event: GenericMessageEvent) {
if (cmd.length > 1) { if (cmd.length > 1) {
val entry: EntryLink = entries[index] val entry: EntryLink = entries.links[entryIndex]
entry.title = cmd.substring(1).trim() entry.title = cmd.substring(1).trim()
bot.updatePin(entry.link, entry) LinksMgr.pinboard.updatePin(event.bot().serverHostname, entry.link, entry)
bot.send(EntriesUtils.buildLink(index, entry)) event.sendMessage(EntriesUtils.buildLink(entryIndex, entry))
LinksMgr.saveEntries(bot, false) entries.save()
} }
} }
private fun changeUrl(cmd: String, login: String, isOp: Boolean, index: Int) { private fun changeUrl(channel: String, cmd: String, entryIndex: Int, event: GenericMessageEvent) {
val entry: EntryLink = entries[index] val entry: EntryLink = entries.links[entryIndex]
if (entry.login == login || isOp) { if (entry.login == event.user.login || isChannelOp(channel, event)) {
val link = cmd.substring(1) val link = cmd.substring(1)
if (link.matches(LinksMgr.LINK_MATCH.toRegex())) { if (link.matches(LinksMgr.LINK_MATCH.toRegex())) {
val oldLink = entry.link val oldLink = entry.link
entry.link = link entry.link = link
bot.updatePin(oldLink, entry) LinksMgr.pinboard.updatePin(event.bot().serverHostname, oldLink, entry)
bot.send(EntriesUtils.buildLink(index, entry)) event.sendMessage(EntriesUtils.buildLink(entryIndex, entry))
LinksMgr.saveEntries(bot, false) entries.save()
} }
} }
} }
private fun changeAuthor(cmd: String, sender: String, isOp: Boolean, index: Int) { private fun changeAuthor(channel: String, cmd: String, index: Int, event: GenericMessageEvent) {
if (isOp) { if (isChannelOp(channel, event)) {
if (cmd.length > 1) { if (cmd.length > 1) {
val entry: EntryLink = entries[index] val entry: EntryLink = entries.links[index]
entry.nick = cmd.substring(1) entry.nick = cmd.substring(1)
bot.send(EntriesUtils.buildLink(index, entry)) LinksMgr.pinboard.updatePin(event.bot().serverHostname, entry.link, entry)
LinksMgr.saveEntries(bot, false) event.sendMessage(EntriesUtils.buildLink(index, entry))
entries.save()
} }
} else { } else {
bot.send(sender, "Please ask a channel op to change the author of this link for you.", false) event.sendMessage("Please ask a channel op to change the author of this link for you.")
} }
} }
private fun removeEntry(sender: String, login: String, isOp: Boolean, index: Int) { private fun removeEntry(channel: String, index: Int, event: GenericMessageEvent) {
val entry: EntryLink = entries[index] val entry: EntryLink = entries.links[index]
if (entry.login == login || isOp) { if (entry.login == event.user.login || isChannelOp(channel, event)) {
bot.deletePin(index, entry) LinksMgr.pinboard.deletePin(entry)
entries.removeAt(index) LinksMgr.twitter.removeEntry(index)
bot.send("Entry ${EntriesUtils.buildLinkCmd(index)} removed.") entries.links.removeAt(index)
LinksMgr.saveEntries(bot, false) event.sendMessage("Entry ${EntriesUtils.buildLinkLabel(index)} removed.")
entries.save()
} else { } else {
bot.send(sender, "Please ask a channel op to remove this entry for you.", false) event.sendMessage("Please ask a channel op to remove this entry for you.")
} }
} }
private fun showEntry(index: Int) { private fun showEntry(index: Int, event: GenericMessageEvent) {
val entry: EntryLink = entries[index] val entry: EntryLink = entries.links[index]
bot.send(EntriesUtils.buildLink(index, entry)) event.sendMessage(EntriesUtils.buildLink(index, entry))
if (entry.tags.isNotEmpty()) { if (entry.tags.isNotEmpty()) {
bot.send(EntriesUtils.buildTags(index, entry)) event.sendMessage(EntriesUtils.buildTags(index, entry))
} }
if (entry.comments.isNotEmpty()) { if (entry.comments.isNotEmpty()) {
val comments = entry.comments val comments = entry.comments
for (i in comments.indices) { for (i in comments.indices) {
bot.send(EntriesUtils.buildComment(index, i, comments[i])) event.sendMessage(EntriesUtils.buildComment(index, i, comments[i]))
} }
} }
} }

View file

@ -33,19 +33,22 @@
package net.thauvin.erik.mobibot.commands.links package net.thauvin.erik.mobibot.commands.links
import net.thauvin.erik.mobibot.Constants import net.thauvin.erik.mobibot.Constants
import net.thauvin.erik.mobibot.Mobibot import net.thauvin.erik.mobibot.Utils.bot
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.sendMessage
import net.thauvin.erik.mobibot.commands.AbstractCommand import net.thauvin.erik.mobibot.commands.AbstractCommand
import net.thauvin.erik.mobibot.entries.EntriesUtils 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
class Tags(bot: Mobibot) : AbstractCommand(bot) { class Tags : AbstractCommand() {
override val name = COMMAND override val name = COMMAND
override val help = listOf( override val help = listOf(
"To categorize or tag a URL, use its label and a ${Constants.TAG_CMD}:", "To categorize or tag a URL, use its label and a ${Constants.TAG_CMD}:",
helpFormat("${Constants.LINK_CMD}1${Constants.TAG_CMD}:<+tag|-tag> [...]") helpFormat("${Constants.LINK_CMD}1${Constants.TAG_CMD}:<+tag|-tag> [...]")
) )
override val isOp = false override val isOpOnly = false
override val isPublic = true override val isPublic = true
override val isVisible = true override val isVisible = true
@ -53,33 +56,27 @@ class Tags(bot: Mobibot) : AbstractCommand(bot) {
const val COMMAND = "tags" const val COMMAND = "tags"
} }
override fun commandResponse( override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
sender: String,
login: String,
args: String,
isOp: Boolean,
isPrivate: Boolean
) {
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
if (index < LinksMgr.entries.size) { if (index < LinksMgr.entries.links.size && LinksMgr.isUpToDate(event)) {
val cmd = cmds[1].trim() val cmd = cmds[1].trim()
val entry: EntryLink = LinksMgr.entries[index] val entry: EntryLink = LinksMgr.entries.links[index]
if (cmd.isNotEmpty()) { if (cmd.isNotEmpty()) {
if (entry.login == login || isOp) { if (entry.login == event.user.login || isChannelOp(channel, event)) {
entry.setTags(cmd) entry.setTags(cmd)
bot.updatePin(entry.link, entry) LinksMgr.pinboard.updatePin(event.bot().serverHostname, entry.link, entry)
bot.send(EntriesUtils.buildTags(index, entry)) event.sendMessage(EntriesUtils.buildTags(index, entry))
LinksMgr.saveEntries(bot, false) LinksMgr.entries.save()
} else { } else {
bot.send(sender, "Please ask a channel op to change the tags for you.", isPrivate) event.sendMessage("Please ask a channel op to change the tags for you.")
} }
} else { } else {
if (entry.tags.isNotEmpty()) { if (entry.tags.isNotEmpty()) {
bot.send(EntriesUtils.buildTags(index, entry)) event.sendMessage(EntriesUtils.buildTags(index, entry))
} else { } else {
bot.send(sender, "The entry has no tags. Why don't add some?", isPrivate) event.sendMessage("The entry has no tags. Why don't add some?")
} }
} }
} }

View file

@ -32,23 +32,25 @@
package net.thauvin.erik.mobibot.commands.links package net.thauvin.erik.mobibot.commands.links
import net.thauvin.erik.mobibot.Mobibot import net.thauvin.erik.mobibot.Utils.bot
import net.thauvin.erik.mobibot.Utils.bold import net.thauvin.erik.mobibot.Utils.buildCmdSyntax
import net.thauvin.erik.mobibot.Utils.helpFormat import net.thauvin.erik.mobibot.Utils.helpFormat
import net.thauvin.erik.mobibot.Utils.sendMessage
import net.thauvin.erik.mobibot.commands.AbstractCommand import net.thauvin.erik.mobibot.commands.AbstractCommand
import net.thauvin.erik.mobibot.commands.links.LinksMgr.Companion.entries import net.thauvin.erik.mobibot.commands.links.LinksMgr.Companion.entries
import net.thauvin.erik.mobibot.entries.EntriesUtils 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.events.PrivateMessageEvent
import org.pircbotx.hooks.types.GenericMessageEvent
class View(bot: Mobibot) : AbstractCommand(bot) { class View : AbstractCommand() {
@Suppress("MagicNumber") private val maxEntries = 6
private val maxEntries = 8
override val name = VIEW_CMD override val name = VIEW_CMD
override val help = listOf( override val help = listOf(
"To list or search the current URL posts:", "To list or search the current URL posts:",
helpFormat("%c $name [<start>] [<query>]") helpFormat("%c $name [<start>] [<query>]")
) )
override val isOp = false override val isOpOnly = false
override val isPublic = true override val isPublic = true
override val isVisible = true override val isVisible = true
@ -56,22 +58,16 @@ class View(bot: Mobibot) : AbstractCommand(bot) {
const val VIEW_CMD = "view" const val VIEW_CMD = "view"
} }
override fun commandResponse( override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
sender: String, if (entries.links.isNotEmpty()) {
login: String, showPosts(args, event)
args: String,
isOp: Boolean,
isPrivate: Boolean
) {
if (entries.size != 0) {
showPosts(bot, args, sender)
} else { } else {
bot.send(sender, "There is currently nothing to view. Why don't you post something?", isPrivate) event.sendMessage("There is currently nothing to view. Why don't you post something?")
} }
} }
private fun showPosts(bot: Mobibot, args: String, sender: String) { private fun showPosts(args: String, event: GenericMessageEvent) {
val max = entries.size val max = entries.links.size
var lcArgs = args.lowercase() var lcArgs = args.lowercase()
var i = 0 var i = 0
if (lcArgs.isEmpty() && max > maxEntries) { if (lcArgs.isEmpty() && max > maxEntries) {
@ -97,25 +93,32 @@ class View(bot: Mobibot) : AbstractCommand(bot) {
var entry: EntryLink var entry: EntryLink
var sent = 0 var sent = 0
while (i < max && sent < maxEntries) { while (i < max && sent < maxEntries) {
entry = entries[i] entry = entries.links[i]
if (lcArgs.isNotBlank()) { if (lcArgs.isNotBlank()) {
if (entry.matches(lcArgs)) { if (entry.matches(lcArgs)) {
bot.send(sender, EntriesUtils.buildLink(i, entry, true), false) event.sendMessage(EntriesUtils.buildLink(i, entry, true))
sent++ sent++
} }
} else { } else {
bot.send(sender, EntriesUtils.buildLink(i, entry, true), false) event.sendMessage(EntriesUtils.buildLink(i, entry, true))
sent++ sent++
} }
i++ i++
if (sent == maxEntries && i < max) { if (sent == maxEntries && i < max) {
bot.send( event.sendMessage("To view more, try: ")
sender, "To view more, try: " + bold("${bot.nick}: $name ${i + 1} $lcArgs"), false event.sendMessage(
helpFormat(
buildCmdSyntax(
"%c $name ${i + 1} $lcArgs",
event.bot().nick,
event is PrivateMessageEvent
)
)
) )
} }
} }
if (sent == 0) { if (sent == 0) {
bot.send(sender, "No matches. Please try again.", false) event.sendMessage("No matches. Please try again.")
} }
} }
} }

View file

@ -31,84 +31,88 @@
*/ */
package net.thauvin.erik.mobibot.commands.tell package net.thauvin.erik.mobibot.commands.tell
import net.thauvin.erik.mobibot.Mobibot
import net.thauvin.erik.mobibot.Utils.bold import net.thauvin.erik.mobibot.Utils.bold
import net.thauvin.erik.mobibot.Utils.buildCmdSyntax import net.thauvin.erik.mobibot.Utils.buildCmdSyntax
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.plural import net.thauvin.erik.mobibot.Utils.plural
import net.thauvin.erik.mobibot.Utils.reverseColor import net.thauvin.erik.mobibot.Utils.reverseColor
import net.thauvin.erik.mobibot.Utils.sendMessage
import net.thauvin.erik.mobibot.Utils.toIntOrDefault import net.thauvin.erik.mobibot.Utils.toIntOrDefault
import net.thauvin.erik.mobibot.Utils.toUtcDateTime import net.thauvin.erik.mobibot.Utils.toUtcDateTime
import net.thauvin.erik.mobibot.commands.AbstractCommand import net.thauvin.erik.mobibot.commands.AbstractCommand
import net.thauvin.erik.mobibot.commands.links.View import net.thauvin.erik.mobibot.commands.links.View
import org.pircbotx.PircBotX
import org.pircbotx.hooks.events.MessageEvent
import org.pircbotx.hooks.types.GenericMessageEvent
import org.pircbotx.hooks.types.GenericUserEvent
/** /**
* The `Tell` command. * The `Tell` command.
*/ */
class Tell(bot: Mobibot) : AbstractCommand(bot) { class Tell(private val serialObject: String) : AbstractCommand() {
// Messages queue // Messages queue
private val messages: MutableList<TellMessage> = mutableListOf() private val messages: MutableList<TellMessage> = mutableListOf()
// Serialized object file
private val serializedObject: String
// Maximum number of days to keep messages // Maximum number of days to keep messages
@Suppress("MagicNumber")
private var maxDays = 7 private var maxDays = 7
// Message maximum queue size // Message maximum queue size
@Suppress("MagicNumber")
private var maxSize = 50 private var maxSize = 50
/** /**
* Cleans the messages queue. * Cleans the messages queue.
*/ */
private fun clean(): Boolean { private fun clean(): Boolean {
if (bot.logger.isDebugEnabled) bot.logger.debug("Cleaning the messages.") // if (bot.logger.isDebugEnabled) bot.logger.debug("Cleaning the messages.")
return TellMessagesMgr.clean(messages, maxDays.toLong()) return TellMessagesMgr.clean(messages, maxDays.toLong())
} }
// Delete message. // Delete message.
private fun deleteMessage(sender: String, args: String, isOp: Boolean, isPrivate: Boolean) { private fun deleteMessage(channel: String, args: String, event: GenericMessageEvent) {
val split = args.split(" ") val split = args.split(" ")
if (split.size == 2) { if (split.size == 2) {
val id = split[1] val id = split[1]
var deleted = false var deleted = false
if (TELL_ALL_KEYWORD.equals(id, ignoreCase = true)) { if (TELL_ALL_KEYWORD.equals(id, ignoreCase = true)) {
for (message in messages) { for (message in messages) {
if (message.sender.equals(sender, ignoreCase = true) && message.isReceived) { if (message.sender.equals(event.user.nick, ignoreCase = true) && message.isReceived) {
messages.remove(message) messages.remove(message)
deleted = true deleted = true
} }
} }
if (deleted) { if (deleted) {
save() save()
bot.send(sender, "Delivered messages have been deleted.", isPrivate) event.sendMessage("Delivered messages have been deleted.")
} else { } else {
bot.send(sender, "No delivered messages were found.", isPrivate) event.sendMessage("No delivered messages were found.")
} }
} else { } else {
var found = false var found = false
for (message in messages) { for (message in messages) {
found = (message.id == id) found = (message.id == id)
if (found && (message.sender.equals(sender, ignoreCase = true) || bot.isOp(sender))) { if (found && (message.sender.equals(event.user.nick, ignoreCase = true) || isChannelOp(
channel,
event
))
) {
messages.remove(message) messages.remove(message)
save() save()
bot.send(sender, "Your message was deleted from the queue.", isPrivate) event.sendMessage("Your message was deleted from the queue.")
deleted = true deleted = true
break break
} }
} }
if (!deleted) { if (!deleted) {
if (found) { if (found) {
bot.send(sender, "Only messages that you sent can be deleted.", isPrivate) event.sendMessage("Only messages that you sent can be deleted.")
} else { } else {
bot.send(sender, "The specified message [ID $id] could not be found.", isPrivate) event.sendMessage("The specified message [ID $id] could not be found.")
} }
} }
} }
} else { } else {
helpResponse(args, sender, isOp, isPrivate) helpResponse(channel, args, event)
} }
} }
@ -124,30 +128,24 @@ class Tell(bot: Mobibot) : AbstractCommand(bot) {
helpFormat("%c $name ${View.VIEW_CMD}"), helpFormat("%c $name ${View.VIEW_CMD}"),
"Messages are kept for ${bold(maxDays)}" + " day".plural(maxDays.toLong()) + '.' "Messages are kept for ${bold(maxDays)}" + " day".plural(maxDays.toLong()) + '.'
) )
override val isOp: Boolean = false override val isOpOnly: Boolean = false
override val isPublic: Boolean = isEnabled() override val isPublic: Boolean = isEnabled()
override val isVisible: Boolean = isEnabled() override val isVisible: Boolean = isEnabled()
override fun commandResponse( override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
sender: String,
login: String,
args: String,
isOp: Boolean,
isPrivate: Boolean
) {
if (isEnabled()) { if (isEnabled()) {
if (args.isBlank()) { if (args.isBlank()) {
helpResponse(args, sender, isOp, isPrivate) helpResponse(channel, args, event)
} else if (args.startsWith(View.VIEW_CMD)) { } else if (args.startsWith(View.VIEW_CMD)) {
if (bot.isOp(sender) && "${View.VIEW_CMD} $TELL_ALL_KEYWORD" == args) { if (isChannelOp(channel, event) && "${View.VIEW_CMD} $TELL_ALL_KEYWORD" == args) {
viewAll(sender, isPrivate) viewAll(event)
} else { } else {
viewMessages(sender, isPrivate) viewMessages(event)
} }
} else if (args.startsWith("$TELL_DEL_KEYWORD ")) { } else if (args.startsWith("$TELL_DEL_KEYWORD ")) {
deleteMessage(sender, args, isOp, isPrivate) deleteMessage(channel, args, event)
} else { } else {
newMessage(sender, args, isOp, isPrivate) newMessage(channel, args, event)
} }
if (clean()) { if (clean()) {
save() save()
@ -169,21 +167,19 @@ class Tell(bot: Mobibot) : AbstractCommand(bot) {
} }
// New message. // New message.
private fun newMessage(sender: String, args: String, isOp: Boolean, isPrivate: Boolean) { private fun newMessage(channel: String, args: String, event: GenericMessageEvent) {
val split = args.split(" ".toRegex(), 2) val split = args.split(" ".toRegex(), 2)
if (split.size == 2 && split[1].isNotBlank() && split[1].contains(" ")) { if (split.size == 2 && split[1].isNotBlank() && split[1].contains(" ")) {
if (messages.size < maxSize) { if (messages.size < maxSize) {
val message = TellMessage(sender, split[0], split[1].trim()) val message = TellMessage(event.user.nick, split[0], split[1].trim())
messages.add(message) messages.add(message)
save() save()
bot.send( event.sendMessage("Message [ID ${message.id}] was queued for ${bold(message.recipient)}")
sender, "Message [ID ${message.id}] was queued for ${bold(message.recipient)}", isPrivate
)
} else { } else {
bot.send(sender, "Sorry, the messages queue is currently full.", isPrivate) event.sendMessage("Sorry, the messages queue is currently full.")
} }
} else { } else {
helpResponse(args, sender, isOp, isPrivate) helpResponse(channel, args, event)
} }
} }
@ -191,34 +187,30 @@ class Tell(bot: Mobibot) : AbstractCommand(bot) {
* Saves the messages queue. * Saves the messages queue.
*/ */
private fun save() { private fun save() {
TellMessagesMgr.save(serializedObject, messages, bot.logger) TellMessagesMgr.save(serialObject, messages)
} }
/** /**
* Checks and sends messages. * Checks and sends messages.
*/ */
@JvmOverloads fun send(event: GenericUserEvent) {
fun send(nickname: String, isMessage: Boolean = false) { val nickname = event.user.nick
if (isEnabled() && nickname != bot.nick) { if (isEnabled() && nickname != event.getBot<PircBotX>().nick) {
messages.stream().filter { message: TellMessage -> message.isMatch(nickname) } messages.stream().filter { message: TellMessage -> message.isMatch(nickname) }
.forEach { message: TellMessage -> .forEach { message: TellMessage ->
if (message.recipient.equals(nickname, ignoreCase = true) && !message.isReceived) { if (message.recipient.equals(nickname, ignoreCase = true) && !message.isReceived) {
if (message.sender == nickname) { if (message.sender == nickname) {
if (!isMessage) { if (event !is MessageEvent) {
bot.send( event.user.send().message(
nickname, "${bold("You")} wanted me to remind you: ${reverseColor(message.message)}"
"${bold("You")} wanted me to remind you: ${reverseColor(message.message)}",
true
) )
message.isReceived = true message.isReceived = true
message.isNotified = true message.isNotified = true
save() save()
} }
} else { } else {
bot.send( event.user.send().message(
nickname, "${message.sender} wanted me to tell you: ${reverseColor(message.message)}"
"${message.sender} wanted me to tell you: ${reverseColor(message.message)}",
true
) )
message.isReceived = true message.isReceived = true
save() save()
@ -226,11 +218,9 @@ class Tell(bot: Mobibot) : AbstractCommand(bot) {
} else if (message.sender.equals(nickname, ignoreCase = true) && message.isReceived } else if (message.sender.equals(nickname, ignoreCase = true) && message.isReceived
&& !message.isNotified && !message.isNotified
) { ) {
bot.send( event.user.send().message(
nickname, "Your message ${reverseColor("[ID ${message.id}]")} was sent to "
"Your message ${reverseColor("[ID " + message.id + ']')} was sent to " + "${bold(message.recipient)} on ${message.receptionDate}"
+ "${bold(message.recipient)} on ${message.receptionDate.toUtcDateTime()}",
true
) )
message.isNotified = true message.isNotified = true
save() save()
@ -247,66 +237,55 @@ class Tell(bot: Mobibot) : AbstractCommand(bot) {
fun size(): Int = messages.size fun size(): Int = messages.size
// View all messages. // View all messages.
private fun viewAll(sender: String, isPrivate: Boolean) { private fun viewAll(event: GenericMessageEvent) {
if (messages.isNotEmpty()) { if (messages.isNotEmpty()) {
for (message in messages) { for (message in messages) {
bot.send( event.sendMessage(
sender, bold(message.sender) + ARROW + bold(message.recipient) "${bold(message.sender)}$ARROW${bold(message.recipient)} [ID: ${message.id}, " +
+ " [ID: " + message.id + ", " (if (message.isReceived) "DELIVERED]" else "QUEUED]")
+ (if (message.isReceived) "DELIVERED" else "QUEUED") + ']',
isPrivate
) )
} }
} else { } else {
bot.send(sender, "There are no messages in the queue.", isPrivate) event.sendMessage("There are no messages in the queue.")
} }
} }
// View messages. // View messages.
private fun viewMessages(sender: String, isPrivate: Boolean) { private fun viewMessages(event: GenericMessageEvent) {
var hasMessage = false var hasMessage = false
for (message in messages) { for (message in messages) {
if (message.isMatch(sender)) { if (message.isMatch(event.user.nick)) {
if (!hasMessage) { if (!hasMessage) {
hasMessage = true hasMessage = true; event.sendMessage("Here are your messages: ")
bot.send(sender, "Here are your messages: ", isPrivate)
} }
if (message.isReceived) { if (message.isReceived) {
bot.send( event.sendMessage(
sender, bold(message.sender) + ARROW + bold(message.recipient) +
bold(message.sender) + ARROW + bold(message.recipient) " [${message.receptionDate.toUtcDateTime()}, ID: ${bold(message.id)}, DELIVERED]"
+ " [${message.receptionDate.toUtcDateTime()}, ID: "
+ bold(message.id) + ", DELIVERED]",
isPrivate
) )
} else { } else {
bot.send( event.sendMessage(
sender, bold(message.sender) + ARROW + bold(message.recipient) +
bold(message.sender) + ARROW + bold(message.recipient) " [${message.queued.toUtcDateTime()}, ID: ${bold(message.id)}, QUEUED]"
+ " [${message.queued.toUtcDateTime()}, ID: "
+ bold(message.id) + ", QUEUED]",
isPrivate
) )
} }
bot.send(sender, helpFormat(message.message), isPrivate) event.sendMessage(helpFormat(message.message))
} }
} }
if (!hasMessage) { if (!hasMessage) {
bot.send(sender, "You have no messages in the queue.", isPrivate) event.sendMessage("You have no messages in the queue.")
} else { } else {
bot.send(sender, "To delete one or all delivered messages:", isPrivate) event.sendMessage("To delete one or all delivered messages:")
bot.send( event.sendMessage(
sender,
helpFormat( helpFormat(
buildCmdSyntax( buildCmdSyntax(
"%c $name $TELL_DEL_KEYWORD <id|$TELL_ALL_KEYWORD>", "%c $name $TELL_DEL_KEYWORD <id|$TELL_ALL_KEYWORD>",
bot.nick, event.user.nick,
isPrivate true
) )
),
isPrivate
) )
bot.send(sender, help.last(), isPrivate) )
event.sendMessage(help.last())
} }
} }
@ -324,9 +303,6 @@ class Tell(bot: Mobibot) : AbstractCommand(bot) {
// Arrow // Arrow
private const val ARROW = " --> " private const val ARROW = " --> "
// Serialized object file extension
private const val SER_EXT = ".ser"
// All keyword // All keyword
private const val TELL_ALL_KEYWORD = "all" private const val TELL_ALL_KEYWORD = "all"
@ -341,8 +317,7 @@ class Tell(bot: Mobibot) : AbstractCommand(bot) {
initProperties(MAX_DAYS_PROP, MAX_SIZE_PROP) initProperties(MAX_DAYS_PROP, MAX_SIZE_PROP)
// Load the message queue // Load the message queue
serializedObject = bot.logsDir + bot.name + SER_EXT messages.addAll(TellMessagesMgr.load(serialObject))
messages.addAll(TellMessagesMgr.load(serializedObject, bot.logger))
if (clean()) { if (clean()) {
save() save()
} }

View file

@ -66,12 +66,12 @@ class TellMessage internal constructor(
var id: String = queued.format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")) var id: String = queued.format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"))
/** /**
* Returns {@code true) if a notification was sent. * Returns {@code true} if a notification was sent.
*/ */
var isNotified = false var isNotified = false
/** /**
* Returns {@code true) if the message was received. * Returns {@code true} if the message was received.
*/ */
var isReceived = false var isReceived = false
set(value) { set(value) {

View file

@ -31,10 +31,10 @@
*/ */
package net.thauvin.erik.mobibot.commands.tell package net.thauvin.erik.mobibot.commands.tell
import org.apache.logging.log4j.Logger import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.io.BufferedInputStream import java.io.BufferedInputStream
import java.io.BufferedOutputStream import java.io.BufferedOutputStream
import java.io.FileNotFoundException
import java.io.IOException import java.io.IOException
import java.io.ObjectInputStream import java.io.ObjectInputStream
import java.io.ObjectOutputStream import java.io.ObjectOutputStream
@ -42,11 +42,14 @@ import java.nio.file.Files
import java.nio.file.Paths import java.nio.file.Paths
import java.time.Clock import java.time.Clock
import java.time.LocalDateTime import java.time.LocalDateTime
import kotlin.io.path.exists
/** /**
* The Tell Messages Manager. * The Tell Messages Manager.
*/ */
object TellMessagesMgr { object TellMessagesMgr {
val logger: Logger = LoggerFactory.getLogger(TellMessagesMgr::class.java)
/** /**
* Cleans the messages queue. * Cleans the messages queue.
*/ */
@ -58,30 +61,30 @@ object TellMessagesMgr {
/** /**
* Loads the messages. * Loads the messages.
*/ */
fun load(file: String): List<TellMessage> {
fun load(file: String, logger: Logger): List<TellMessage> { val serialFile = Paths.get(file)
if (serialFile.exists()) {
try { try {
ObjectInputStream( ObjectInputStream(
BufferedInputStream(Files.newInputStream(Paths.get(file))) BufferedInputStream(Files.newInputStream(serialFile))
).use { input -> ).use { input ->
if (logger.isDebugEnabled) logger.debug("Loading the messages.") if (logger.isDebugEnabled) logger.debug("Loading the messages.")
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
return input.readObject() as List<TellMessage> return input.readObject() as List<TellMessage>
} }
} catch (ignore: FileNotFoundException) {
// Do nothing
} catch (e: IOException) { } catch (e: IOException) {
logger.error("An IO error occurred loading the messages queue.", e) logger.error("An IO error occurred loading the messages queue.", e)
} catch (e: ClassNotFoundException) { } catch (e: ClassNotFoundException) {
logger.error("An error occurred loading the messages queue.", e) logger.error("An error occurred loading the messages queue.", e)
} }
}
return listOf() return listOf()
} }
/** /**
* Saves the messages. * Saves the messages.
*/ */
fun save(file: String, messages: List<TellMessage?>?, logger: Logger) { fun save(file: String, messages: List<TellMessage?>?) {
try { try {
BufferedOutputStream(Files.newOutputStream(Paths.get(file))).use { bos -> BufferedOutputStream(Files.newOutputStream(Paths.get(file))).use { bos ->
ObjectOutputStream(bos).use { output -> ObjectOutputStream(bos).use { output ->

View file

@ -1,5 +1,5 @@
/* /*
* Kill.kt * Entries.kt
* *
* Copyright (c) 2004-2021, Erik C. Thauvin (erik@thauvin.net) * Copyright (c) 2004-2021, Erik C. Thauvin (erik@thauvin.net)
* All rights reserved. * All rights reserved.
@ -30,26 +30,26 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/ */
package net.thauvin.erik.mobibot.commands package net.thauvin.erik.mobibot.entries
import net.thauvin.erik.mobibot.Mobibot import net.thauvin.erik.mobibot.Utils.today
class Kill(bot: Mobibot) : AbstractCommand(bot) { class Entries(
override val name = "kill" var channel: String = "",
override val help = emptyList<String>() var ircServer: String = "",
override val isOp = true var logsDir: String = "",
override val isPublic = false var backlogs: String = ""
override val isVisible = false
override fun commandResponse(
sender: String,
login: String,
args: String,
isOp: Boolean,
isPrivate: Boolean
) { ) {
if (isOp) { val links = mutableListOf<EntryLink>()
bot.shutdown(sender, true)
} var lastPubDate = today()
fun load() {
lastPubDate = FeedsMgr.loadFeed(this)
}
fun save() {
lastPubDate = today()
FeedsMgr.saveFeed(this)
} }
} }

View file

@ -1,262 +0,0 @@
/*
* EntriesMgr.kt
*
* Copyright (c) 2004-2021, Erik C. Thauvin (erik@thauvin.net)
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* Neither the name of this project nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.thauvin.erik.mobibot.entries
import com.rometools.rome.feed.synd.SyndContentImpl
import com.rometools.rome.feed.synd.SyndEntry
import com.rometools.rome.feed.synd.SyndEntryImpl
import com.rometools.rome.feed.synd.SyndFeed
import com.rometools.rome.feed.synd.SyndFeedImpl
import com.rometools.rome.io.FeedException
import com.rometools.rome.io.SyndFeedInput
import com.rometools.rome.io.SyndFeedOutput
import net.thauvin.erik.mobibot.Mobibot
import net.thauvin.erik.mobibot.Utils.toIsoLocalDate
import java.io.IOException
import java.io.InputStreamReader
import java.io.OutputStreamWriter
import java.nio.charset.StandardCharsets
import java.nio.file.Files
import java.nio.file.Paths
import java.util.Calendar
/**
* Manages the feed entries.
*/
object EntriesMgr {
/**
* The name of the file containing the current entries.
*/
const val CURRENT_XML = "current.xml"
/**
* The name of the file containing the backlog entries.
*/
const val NAV_XML = "nav.xml"
/**
* The .xml extension
*/
const val XML_EXT = ".xml"
// Maximum number of backlogs to keep
private const val maxBacklogs = 10
// Daily backup
private fun dailyBackup(
bot: Mobibot,
history: MutableList<String>
) {
if (bot.backlogsUrl.isNotBlank()) {
if (!history.contains(bot.today)) {
history.add(bot.today)
while (history.size > maxBacklogs) {
history.removeFirst()
}
}
OutputStreamWriter(
Files.newOutputStream(Paths.get(bot.logsDir + NAV_XML)), StandardCharsets.UTF_8
).use { fw ->
val output = SyndFeedOutput()
val rss: SyndFeed = SyndFeedImpl()
val items: MutableList<SyndEntry> = mutableListOf()
var item: SyndEntry
with(rss) {
feedType = "rss_2.0"
title = "${bot.channel} IRC Links Backlogs"
description = "Backlogs of Links from ${bot.ircServer} on ${bot.channel}"
link = bot.backlogsUrl
publishedDate = Calendar.getInstance().time
}
var date: String
items.clear()
for (i in history.size - 1 downTo 0) {
date = history[i]
item = SyndEntryImpl()
with(item) {
link = bot.backlogsUrl + date + ".xml"
title = date
description = SyndContentImpl().apply { value = "Links for $date" }
}
items.add(item)
}
rss.entries = items
if (bot.logger.isDebugEnabled) bot.logger.debug("Writing the backlog feed.")
output.output(rss, fw)
}
} else {
if (bot.logger.isErrorEnabled) {
bot.logger.warn("Unable to generate the backlogs feed. No property configured.")
}
}
}
/**
* Loads the backlogs.
*/
@Throws(IOException::class, FeedException::class)
fun loadBacklogs(file: String, history: MutableList<String>) {
history.clear()
val input = SyndFeedInput()
InputStreamReader(Files.newInputStream(Paths.get(file)), StandardCharsets.UTF_8).use { reader ->
val feed = input.build(reader)
val items = feed.entries
for (i in items.indices.reversed()) {
history.add(items[i].title)
}
}
}
/**
* Loads the current entries.
*/
@Throws(IOException::class, FeedException::class)
fun loadEntries(file: String, channel: String, entries: MutableList<EntryLink>): String {
entries.clear()
val input = SyndFeedInput()
var today: String
InputStreamReader(
Files.newInputStream(Paths.get(file)), StandardCharsets.UTF_8
).use { reader ->
val feed = input.build(reader)
today = feed.publishedDate.toIsoLocalDate()
val items = feed.entries
var entry: EntryLink
for (i in items.indices.reversed()) {
with(items[i]) {
entry = EntryLink(
link,
title,
author.substring(author.lastIndexOf('(') + 1, author.length - 1),
channel,
publishedDate,
categories
)
var split: List<String>
for (comment in description.value.split("<br/>")) {
split = comment.split(": ".toRegex(), 2)
if (split.size == 2) {
entry.addComment(comment = split[1].trim(), nick = split[0].trim())
}
}
}
entries.add(entry)
}
}
return today
}
/**
* Saves the entries.
*/
fun saveEntries(
bot: Mobibot,
entries: List<EntryLink>,
history: MutableList<String>,
isDayBackup: Boolean
) {
if (bot.logger.isDebugEnabled) bot.logger.debug("Saving the feeds...")
if (bot.logsDir.isNotBlank() && bot.weblogUrl.isNotBlank()) {
try {
val output = SyndFeedOutput()
val rss: SyndFeed = SyndFeedImpl()
val items: MutableList<SyndEntry> = mutableListOf()
var item: SyndEntry
OutputStreamWriter(
Files.newOutputStream(Paths.get(bot.logsDir + CURRENT_XML)), StandardCharsets.UTF_8
).use { fw ->
with(rss) {
feedType = "rss_2.0"
title = bot.channel + " IRC Links"
description = "Links from ${bot.ircServer} on ${bot.channel}"
link = bot.weblogUrl
publishedDate = Calendar.getInstance().time
language = "en"
}
val buff: StringBuilder = StringBuilder()
for (i in entries.size - 1 downTo 0) {
with(entries[i]) {
buff.setLength(0)
buff.append("Posted by <b>")
.append(nick)
.append("</b> on <a href=\"irc://")
.append(bot.ircServer).append('/')
.append(channel)
.append("\"><b>")
.append(channel)
.append("</b></a>")
if (comments.size > 0) {
buff.append(" <br/><br/>")
for (j in comments.indices) {
if (j > 0) {
buff.append(" <br/>")
}
buff.append(comments[j].nick).append(": ").append(comments[j].comment)
}
}
item = SyndEntryImpl()
item.link = link
item.description = SyndContentImpl().apply { value = buff.toString() }
item.title = title
item.publishedDate = date
item.author = "${bot.channel.substring(1)}@${bot.ircServer} ($nick)"
item.categories = tags
items.add(item)
}
}
rss.entries = items
if (bot.logger.isDebugEnabled) bot.logger.debug("Writing the entries feed.")
output.output(rss, fw)
}
OutputStreamWriter(
Files.newOutputStream(
Paths.get(
bot.logsDir + bot.today + XML_EXT
)
), StandardCharsets.UTF_8
).use { fw -> output.output(rss, fw) }
if (isDayBackup) {
dailyBackup(bot, history)
}
} catch (e: FeedException) {
if (bot.logger.isWarnEnabled) bot.logger.warn("Unable to generate the entries feed.", e)
} catch (e: IOException) {
if (bot.logger.isWarnEnabled)
bot.logger.warn("An IO error occurred while generating the entries feed.", e)
}
} else {
if (bot.logger.isWarnEnabled) {
bot.logger.warn("Unable to generate the entries feed. A required property is missing.")
}
}
}
}

View file

@ -40,22 +40,22 @@ import net.thauvin.erik.mobibot.Utils.green
*/ */
object EntriesUtils { object EntriesUtils {
/** /**
* Build link cmd based on its index. e.g: L1 * Build link label based on its index. e.g: L1
*/ */
fun buildLinkCmd(index: Int): String = Constants.LINK_CMD + (index + 1) fun buildLinkLabel(index: Int): String = Constants.LINK_CMD + (index + 1)
/** /**
* Builds an entry's comment for display on the channel. * Builds an entry's comment for display on the channel.
*/ */
fun buildComment(entryIndex: Int, commentIndex: Int, comment: EntryComment): String = fun buildComment(entryIndex: Int, commentIndex: Int, comment: EntryComment): String =
("${buildLinkCmd(entryIndex)}.${commentIndex + 1}: [${comment.nick}] ${comment.comment}") ("${buildLinkLabel(entryIndex)}.${commentIndex + 1}: [${comment.nick}] ${comment.comment}")
/** /**
* Builds an entry's link for display on the channel. * Builds an entry's link for display on the channel.
*/ */
@JvmOverloads @JvmOverloads
fun buildLink(entryIndex: Int, entry: EntryLink, isView: Boolean = false): String { fun buildLink(entryIndex: Int, entry: EntryLink, isView: Boolean = false): String {
val buff = StringBuilder().append(buildLinkCmd(entryIndex)).append(": ") val buff = StringBuilder().append(buildLinkLabel(entryIndex)).append(": ")
.append('[').append(entry.nick).append(']') .append('[').append(entry.nick).append(']')
if (isView && entry.comments.isNotEmpty()) { if (isView && entry.comments.isNotEmpty()) {
buff.append("[+").append(entry.comments.size).append(']') buff.append("[+").append(entry.comments.size).append(']')
@ -76,5 +76,5 @@ object EntriesUtils {
* Build an entry's tags/categories for display on the channel. * Build an entry's tags/categories for display on the channel.
*/ */
fun buildTags(entryIndex: Int, entry: EntryLink): String = fun buildTags(entryIndex: Int, entry: EntryLink): String =
buildLinkCmd(entryIndex) + "${Constants.TAG_CMD}: " + entry.pinboardTags.replace(",", ", ") buildLinkLabel(entryIndex) + "${Constants.TAG_CMD}: " + entry.pinboardTags.replace(",", ", ")
} }

View file

@ -109,16 +109,25 @@ class EntryLink : Serializable {
*/ */
fun addComment(comment: String, nick: String): Int { fun addComment(comment: String, nick: String): Int {
comments.add(EntryComment(comment, nick)) comments.add(EntryComment(comment, nick))
return comments.size - 1 return comments.lastIndex
} }
/** /**
* Deletes a specific comment. * Deletes a specific comment.
*/ */
fun deleteComment(index: Int) { fun deleteComment(index: Int): Boolean {
if (index < comments.size) { if (index < comments.size) {
comments.removeAt(index) comments.removeAt(index)
return true
} }
return false
}
/**
* Deletes a comment.
*/
fun deleteComment(entryComment: EntryComment): Boolean {
return comments.remove(entryComment)
} }
/** /**
@ -154,7 +163,7 @@ class EntryLink : Serializable {
* Sets a comment. * Sets a comment.
*/ */
fun setComment(index: Int, comment: String?, nick: String?) { fun setComment(index: Int, comment: String?, nick: String?) {
if (index < comments.size && (comment != null) && !nick.isNullOrBlank()) { if (index < comments.size && !comment.isNullOrBlank() && !nick.isNullOrBlank()) {
comments[index] = EntryComment(comment, nick) comments[index] = EntryComment(comment, nick)
} }
} }

View file

@ -0,0 +1,190 @@
/*
* FeedsMgr.kt
*
* Copyright (c) 2004-2021, Erik C. Thauvin (erik@thauvin.net)
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* Neither the name of this project nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.thauvin.erik.mobibot.entries
import com.rometools.rome.feed.synd.SyndContentImpl
import com.rometools.rome.feed.synd.SyndEntry
import com.rometools.rome.feed.synd.SyndEntryImpl
import com.rometools.rome.feed.synd.SyndFeed
import com.rometools.rome.feed.synd.SyndFeedImpl
import com.rometools.rome.io.FeedException
import com.rometools.rome.io.SyndFeedInput
import com.rometools.rome.io.SyndFeedOutput
import net.thauvin.erik.mobibot.Utils.toIsoLocalDate
import net.thauvin.erik.mobibot.Utils.today
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.io.IOException
import java.io.InputStreamReader
import java.io.OutputStreamWriter
import java.nio.charset.StandardCharsets
import java.nio.file.Files
import java.nio.file.Paths
import java.util.Calendar
import kotlin.io.path.exists
/**
* Manages the RSS feeds.
*/
class FeedsMgr private constructor() {
companion object {
private val logger: Logger = LoggerFactory.getLogger(FeedsMgr::class.java)
// The file containing the current entries.
private const val currentXml = "current.xml"
// The .xml extension.
private const val dotXml = ".xml"
/**
* Loads the current feed.
*/
@Throws(IOException::class, FeedException::class)
fun loadFeed(entries: Entries, currentFile: String = currentXml): String {
entries.links.clear()
val xml = Paths.get("${entries.logsDir}${currentFile}")
var pubDate = today()
if (xml.exists()) {
val input = SyndFeedInput()
InputStreamReader(
Files.newInputStream(xml), StandardCharsets.UTF_8
).use { reader ->
val feed = input.build(reader)
pubDate = feed.publishedDate.toIsoLocalDate()
val items = feed.entries
var entry: EntryLink
for (i in items.indices.reversed()) {
with(items[i]) {
entry = EntryLink(
link,
title,
author.substring(author.lastIndexOf('(') + 1, author.length - 1),
entries.channel,
publishedDate,
categories
)
var split: List<String>
for (comment in description.value.split("<br/>")) {
split = comment.split(": ".toRegex(), 2)
if (split.size == 2) {
entry.addComment(comment = split[1].trim(), nick = split[0].trim())
}
}
}
entries.links.add(entry)
}
}
} else {
// Create an empty feed.
saveFeed(entries)
}
return pubDate
}
/**
* Saves the feeds.
*/
fun saveFeed(entries: Entries, currentFile: String = currentXml) {
if (logger.isDebugEnabled) logger.debug("Saving the feeds...")
if (entries.logsDir.isNotBlank()) {
try {
val output = SyndFeedOutput()
val rss: SyndFeed = SyndFeedImpl()
val items: MutableList<SyndEntry> = mutableListOf()
var item: SyndEntry
OutputStreamWriter(
Files.newOutputStream(Paths.get("${entries.logsDir}${currentFile}")), StandardCharsets.UTF_8
).use { fw ->
with(rss) {
feedType = "rss_2.0"
title = "${entries.channel} IRC Links"
description = "Links from ${entries.ircServer} on ${entries.channel}"
if (entries.backlogs.isNotBlank()) link = entries.backlogs
publishedDate = Calendar.getInstance().time
language = "en"
}
val buff: StringBuilder = StringBuilder()
for (i in entries.links.indices.reversed()) {
with(entries.links[i]) {
buff.setLength(0)
buff.append("Posted by <b>")
.append(nick)
.append("</b> on <a href=\"irc://")
.append(entries.ircServer).append('/')
.append(channel)
.append("\"><b>")
.append(channel)
.append("</b></a>")
if (comments.size > 0) {
buff.append(" <br/><br/>")
for (j in comments.indices) {
if (j > 0) {
buff.append(" <br/>")
}
buff.append(comments[j].nick).append(": ").append(comments[j].comment)
}
}
item = SyndEntryImpl()
item.link = link
item.description = SyndContentImpl().apply { value = buff.toString() }
item.title = title
item.publishedDate = date
item.author = "${channel.substring(1)}@${entries.ircServer} ($nick)"
item.categories = tags
items.add(item)
}
}
rss.entries = items
if (logger.isDebugEnabled) logger.debug("Writing the entries feed.")
output.output(rss, fw)
}
OutputStreamWriter(
Files.newOutputStream(
Paths.get(
entries.logsDir + today() + dotXml
)
), StandardCharsets.UTF_8
).use { fw -> output.output(rss, fw) }
} catch (e: FeedException) {
if (logger.isWarnEnabled) logger.warn("Unable to generate the entries feed.", e)
} catch (e: IOException) {
if (logger.isWarnEnabled)
logger.warn("An IO error occurred while generating the entries feed.", e)
}
} else {
if (logger.isWarnEnabled) {
logger.warn("Unable to generate the entries feed. A required property is missing.")
}
}
}
}
}

View file

@ -31,13 +31,16 @@
*/ */
package net.thauvin.erik.mobibot.modules package net.thauvin.erik.mobibot.modules
import net.thauvin.erik.mobibot.Mobibot import net.thauvin.erik.mobibot.Utils.bot
import net.thauvin.erik.mobibot.Utils.buildCmdSyntax import net.thauvin.erik.mobibot.Utils.buildCmdSyntax
import net.thauvin.erik.mobibot.Utils.sendMessage
import org.pircbotx.hooks.events.PrivateMessageEvent
import org.pircbotx.hooks.types.GenericMessageEvent
/** /**
* The `Module` abstract class. * The `Module` abstract class.
*/ */
abstract class AbstractModule(val bot: Mobibot) { abstract class AbstractModule {
/** /**
* The module's commands, if any. * The module's commands, if any.
*/ */
@ -51,12 +54,7 @@ abstract class AbstractModule(val bot: Mobibot) {
/** /**
* Responds to a command. * Responds to a command.
*/ */
abstract fun commandResponse( abstract fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent)
sender: String,
cmd: String,
args: String,
isPrivate: Boolean
)
/** /**
* Returns the module's property keys. * Returns the module's property keys.
@ -74,9 +72,9 @@ abstract class AbstractModule(val bot: Mobibot) {
/** /**
* Responds with the module's help. * Responds with the module's help.
*/ */
open fun helpResponse(sender: String, isPrivate: Boolean): Boolean { open fun helpResponse(event: GenericMessageEvent): Boolean {
for (h in help) { for (h in help) {
bot.send(sender, buildCmdSyntax(h, bot.nick, isPrivateMsgEnabled && isPrivate), isPrivate) event.sendMessage(buildCmdSyntax(h, event.bot().nick, isPrivateMsgEnabled && event is PrivateMessageEvent))
} }
return true return true
} }

View file

@ -33,35 +33,32 @@ package net.thauvin.erik.mobibot.modules
import net.objecthunter.exp4j.ExpressionBuilder import net.objecthunter.exp4j.ExpressionBuilder
import net.objecthunter.exp4j.tokenizer.UnknownFunctionOrVariableException import net.objecthunter.exp4j.tokenizer.UnknownFunctionOrVariableException
import net.thauvin.erik.mobibot.Mobibot
import net.thauvin.erik.mobibot.Utils.bold import net.thauvin.erik.mobibot.Utils.bold
import net.thauvin.erik.mobibot.Utils.helpFormat import net.thauvin.erik.mobibot.Utils.helpFormat
import org.pircbotx.hooks.types.GenericMessageEvent
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.text.DecimalFormat import java.text.DecimalFormat
/** /**
* The Calc module. * The Calc module.
*/ */
class Calc(bot: Mobibot) : AbstractModule(bot) { class Calc : AbstractModule() {
override fun commandResponse( private val logger: Logger = LoggerFactory.getLogger(Calc::class.java)
sender: String,
cmd: String, override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
args: String,
isPrivate: Boolean
) {
if (args.isNotBlank()) { if (args.isNotBlank()) {
with(bot) {
try { try {
send(calculate(args)) event.respond(calculate(args))
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
if (logger.isWarnEnabled) logger.warn("Failed to calculate: $args", e) if (logger.isWarnEnabled) logger.warn("Failed to calculate: $args", e)
send("No idea. This is the kind of math I don't get.") event.respond("No idea. This is the kind of math I don't get.")
} catch (e: UnknownFunctionOrVariableException) { } catch (e: UnknownFunctionOrVariableException) {
if (logger.isWarnEnabled) logger.warn("Unable to calculate: $args", e) if (logger.isWarnEnabled) logger.warn("Unable to calculate: $args", e)
send("No idea. I must've some form of Dyscalculia.") event.respond("No idea. I must've some form of Dyscalculia.")
}
} }
} else { } else {
helpResponse(sender, isPrivate) helpResponse(event)
} }
} }

View file

@ -34,20 +34,24 @@ package net.thauvin.erik.mobibot.modules
import net.thauvin.erik.crypto.CryptoException import net.thauvin.erik.crypto.CryptoException
import net.thauvin.erik.crypto.CryptoPrice import net.thauvin.erik.crypto.CryptoPrice
import net.thauvin.erik.crypto.CryptoPrice.Companion.spotPrice import net.thauvin.erik.crypto.CryptoPrice.Companion.spotPrice
import net.thauvin.erik.mobibot.Mobibot
import net.thauvin.erik.mobibot.Utils.helpFormat import net.thauvin.erik.mobibot.Utils.helpFormat
import net.thauvin.erik.mobibot.msg.PublicMessage import net.thauvin.erik.mobibot.Utils.sendMessage
import org.pircbotx.hooks.types.GenericMessageEvent
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.io.IOException
/** /**
* The Cryptocurrency Prices module. * The Cryptocurrency Prices module.
*/ */
class CryptoPrices(bot: Mobibot) : ThreadedModule(bot) { class CryptoPrices : ThreadedModule() {
private val logger: Logger = LoggerFactory.getLogger(CryptoPrices::class.java)
/** /**
* Returns the cryptocurrency market price from [Coinbase](https://developers.coinbase.com/api/v2#get-spot-price). * Returns the cryptocurrency market price from [Coinbase](https://developers.coinbase.com/api/v2#get-spot-price).
*/ */
override fun run(sender: String, cmd: String, args: String, isPrivate: Boolean) { override fun run(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
val debugMessage = "crypto($cmd $args)" val debugMessage = "crypto($cmd $args)"
with(bot) {
if (args.matches("\\w+( [a-zA-Z]{3}+)?".toRegex())) { if (args.matches("\\w+( [a-zA-Z]{3}+)?".toRegex())) {
try { try {
val price = currentPrice(args.split(' ')) val price = currentPrice(args.split(' '))
@ -56,18 +60,18 @@ class CryptoPrices(bot: Mobibot) : ThreadedModule(bot) {
} catch (ignore: IllegalArgumentException) { } catch (ignore: IllegalArgumentException) {
price.amount price.amount
} }
send(sender, PublicMessage("${price.base}: $amount [${price.currency}]")) event.respond("${price.base} current price is $amount [${price.currency}]")
} catch (e: CryptoException) { } catch (e: CryptoException) {
if (logger.isWarnEnabled) logger.warn("$debugMessage => ${e.statusCode}", e) if (logger.isWarnEnabled) logger.warn("$debugMessage => ${e.statusCode}", e)
send(e.message) event.sendMessage(e.message!!)
} catch (e: Exception) { } catch (e: IOException) {
if (logger.isErrorEnabled) logger.error(debugMessage, e) if (logger.isErrorEnabled) logger.error(debugMessage, e)
send("An error has occurred while retrieving the cryptocurrency market price.") event.sendMessage("An IO error has occurred while retrieving the cryptocurrency market price.")
} }
} else { } else {
helpResponse(sender, isPrivate) helpResponse(event)
}
} }
} }
companion object { companion object {

View file

@ -31,16 +31,21 @@
*/ */
package net.thauvin.erik.mobibot.modules package net.thauvin.erik.mobibot.modules
import net.thauvin.erik.mobibot.Mobibot
import net.thauvin.erik.mobibot.Utils.bold import net.thauvin.erik.mobibot.Utils.bold
import net.thauvin.erik.mobibot.Utils.bot
import net.thauvin.erik.mobibot.Utils.buildCmdSyntax import net.thauvin.erik.mobibot.Utils.buildCmdSyntax
import net.thauvin.erik.mobibot.Utils.helpFormat import net.thauvin.erik.mobibot.Utils.helpFormat
import net.thauvin.erik.mobibot.Utils.sendList
import net.thauvin.erik.mobibot.Utils.sendMessage
import net.thauvin.erik.mobibot.Utils.today import net.thauvin.erik.mobibot.Utils.today
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.jdom2.JDOMException import org.jdom2.JDOMException
import org.jdom2.input.SAXBuilder import org.jdom2.input.SAXBuilder
import org.pircbotx.hooks.types.GenericMessageEvent
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.io.IOException import java.io.IOException
import java.net.URL import java.net.URL
import java.text.NumberFormat import java.text.NumberFormat
@ -51,26 +56,22 @@ import javax.xml.XMLConstants
/** /**
* The CurrencyConverter module. * The CurrencyConverter module.
*/ */
class CurrencyConverter(bot: Mobibot) : ThreadedModule(bot) { class CurrencyConverter : ThreadedModule() {
override fun commandResponse( private val logger: Logger = LoggerFactory.getLogger(CurrencyConverter::class.java)
sender: String,
cmd: String, override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
args: String,
isPrivate: Boolean
) {
synchronized(this) { synchronized(this) {
if (pubDate != today()) { if (pubDate != today()) {
EXCHANGE_RATES.clear() EXCHANGE_RATES.clear()
} }
} }
super.commandResponse(sender, cmd, args, isPrivate) super.commandResponse(channel, cmd, args, event)
} }
/** /**
* Converts the specified currencies. * Converts the specified currencies.
*/ */
override fun run(sender: String, cmd: String, args: String, isPrivate: Boolean) { override fun run(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
bot.apply {
if (EXCHANGE_RATES.isEmpty()) { if (EXCHANGE_RATES.isEmpty()) {
try { try {
loadRates() loadRates()
@ -80,55 +81,43 @@ class CurrencyConverter(bot: Mobibot) : ThreadedModule(bot) {
} }
if (EXCHANGE_RATES.isEmpty()) { if (EXCHANGE_RATES.isEmpty()) {
send(sender, EMPTY_RATE_TABLE, true) event.respond(EMPTY_RATE_TABLE)
} else if (args.matches("\\d+([,\\d]+)?(\\.\\d+)? [a-zA-Z]{3}+ to [a-zA-Z]{3}+".toRegex())) { } else if (args.matches("\\d+([,\\d]+)?(\\.\\d+)? [a-zA-Z]{3}+ to [a-zA-Z]{3}+".toRegex())) {
val msg = convertCurrency(args) val msg = convertCurrency(args)
send(sender, msg) event.respond(msg.msg)
if (msg.isError) { if (msg.isError) {
helpResponse(sender, isPrivate) helpResponse(event)
} }
} else if (args.contains(CURRENCY_RATES_KEYWORD)) { } else if (args.contains(CURRENCY_RATES_KEYWORD)) {
send(sender, "The reference rates for ${bold(pubDate)} are:", isPrivate) event.sendMessage("The reference rates for ${bold(pubDate)} are:")
@Suppress("MagicNumber") event.sendList(currencyRates(), 3, " ", isIndent = true)
sendList(sender, currencyRates(), 3, " ", isPrivate, isIndent = true)
} else { } else {
helpResponse(sender, isPrivate) helpResponse(event)
}
} }
} }
override fun helpResponse(sender: String, isPrivate: Boolean): Boolean { override fun helpResponse(event: GenericMessageEvent): Boolean {
with(bot) {
if (EXCHANGE_RATES.isEmpty()) { if (EXCHANGE_RATES.isEmpty()) {
try { try {
loadRates() loadRates()
} catch (e: ModuleException) { } catch (e: ModuleException) {
if (logger.isDebugEnabled) logger.debug(e.debugMessage, e) if (logger.isWarnEnabled) logger.warn(e.debugMessage, e)
} }
} }
if (EXCHANGE_RATES.isEmpty()) { if (EXCHANGE_RATES.isEmpty()) {
send(sender, EMPTY_RATE_TABLE, isPrivate) event.sendMessage(EMPTY_RATE_TABLE)
} else { } else {
send(sender, "To convert from one currency to another:", isPrivate) val nick = event.bot().nick
send( event.sendMessage("To convert from one currency to another:")
sender, event.sendMessage(helpFormat(buildCmdSyntax("%c $CURRENCY_CMD 100 USD to EUR", nick, isPrivateMsgEnabled)))
helpFormat( event.sendMessage("For a listing of current reference rates:")
buildCmdSyntax("%c $CURRENCY_CMD 100 USD to EUR", nick, isPrivateMsgEnabled) event.sendMessage(
),
isPrivate
)
send(sender, "For a listing of current reference rates:", isPrivate)
send(
sender,
helpFormat( helpFormat(
buildCmdSyntax("%c $CURRENCY_CMD $CURRENCY_RATES_KEYWORD", nick, isPrivateMsgEnabled) buildCmdSyntax("%c $CURRENCY_CMD $CURRENCY_RATES_KEYWORD", nick, isPrivateMsgEnabled)
),
isPrivate
) )
send(sender, "The supported currencies are: ", isPrivate) )
@Suppress("MagicNumber") event.sendMessage("The supported currencies are: ")
sendList(sender, ArrayList(EXCHANGE_RATES.keys), 11, isPrivate = isPrivate, isIndent = true) event.sendList(ArrayList(EXCHANGE_RATES.keys), 11, isIndent = true)
}
} }
return true return true
} }
@ -161,7 +150,6 @@ class CurrencyConverter(bot: Mobibot) : ThreadedModule(bot) {
/** /**
* Converts from a currency to another. * Converts from a currency to another.
*/ */
@Suppress("MagicNumber")
@JvmStatic @JvmStatic
fun convertCurrency(query: String): Message { fun convertCurrency(query: String): Message {
val cmds = query.split(" ") val cmds = query.split(" ")
@ -194,7 +182,6 @@ class CurrencyConverter(bot: Mobibot) : ThreadedModule(bot) {
fun currencyRates(): List<String> { fun currencyRates(): List<String> {
val rates = mutableListOf<String>() val rates = mutableListOf<String>()
for ((key, value) in EXCHANGE_RATES.toSortedMap()) { for ((key, value) in EXCHANGE_RATES.toSortedMap()) {
@Suppress("MagicNumber")
rates.add("$key: ${value.padStart(8)}") rates.add("$key: ${value.padStart(8)}")
} }
return rates return rates

View file

@ -31,37 +31,33 @@
*/ */
package net.thauvin.erik.mobibot.modules package net.thauvin.erik.mobibot.modules
import net.thauvin.erik.mobibot.Mobibot import net.thauvin.erik.mobibot.Utils.bold
import net.thauvin.erik.mobibot.Utils.bot
import net.thauvin.erik.mobibot.Utils.helpFormat import net.thauvin.erik.mobibot.Utils.helpFormat
import org.pircbotx.hooks.types.GenericMessageEvent
import kotlin.random.Random import kotlin.random.Random
/** /**
* The Dice module. * The Dice module.
*/ */
class Dice(bot: Mobibot) : AbstractModule(bot) { class Dice : AbstractModule() {
override fun commandResponse( override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
sender: String,
cmd: String,
args: String,
isPrivate: Boolean
) {
val botRoll = roll() val botRoll = roll()
val roll = roll() val roll = roll()
val botTotal = botRoll.first + botRoll.second val botTotal = botRoll.first + botRoll.second
val total = roll.first + roll.second val total = roll.first + roll.second
with(bot) { with(event.bot()) {
send( event.respond(
channel, "you rolled ${DICE_FACES[roll.first]} ${DICE_FACES[roll.second]} for a total of ${bold(total)}"
"$sender rolled ${total}: ${DICE_FACES[roll.first]} ${DICE_FACES[roll.second]}",
isPrivate
) )
action( sendIRC().action(
"rolled ${botTotal}: ${DICE_FACES[botRoll.first]} ${DICE_FACES[botRoll.second]}" channel,
"rolled ${DICE_FACES[botRoll.first]} ${DICE_FACES[botRoll.second]} for a total of ${bold(botTotal)}"
) )
when (winLoseOrTie(botTotal, total)) { when (winLoseOrTie(botTotal, total)) {
Result.WIN -> action("wins.") Result.WIN -> sendIRC().action(channel, "wins.")
Result.LOSE -> action("lost.") Result.LOSE -> sendIRC().action(channel, "lost.")
else -> action("tied.") else -> sendIRC().action(channel, "tied.")
} }
} }
} }

View file

@ -31,29 +31,34 @@
*/ */
package net.thauvin.erik.mobibot.modules package net.thauvin.erik.mobibot.modules
import net.thauvin.erik.mobibot.Mobibot
import net.thauvin.erik.mobibot.Utils.capitalise 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.sendMessage
import net.thauvin.erik.mobibot.Utils.unescapeXml import net.thauvin.erik.mobibot.Utils.unescapeXml
import net.thauvin.erik.mobibot.Utils.urlReader import net.thauvin.erik.mobibot.Utils.urlReader
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.NoticeMessage import net.thauvin.erik.mobibot.msg.NoticeMessage
import org.jibble.pircbot.Colors
import org.json.JSONException import org.json.JSONException
import org.json.JSONObject import org.json.JSONObject
import org.pircbotx.Colors
import org.pircbotx.hooks.types.GenericMessageEvent
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.io.IOException import java.io.IOException
import java.net.URL import java.net.URL
/** /**
* The GoogleSearch module. * The GoogleSearch module.
*/ */
class GoogleSearch(bot: Mobibot) : ThreadedModule(bot) { class GoogleSearch : ThreadedModule() {
private val logger: Logger = LoggerFactory.getLogger(GoogleSearch::class.java)
/** /**
* Searches Google. * Searches Google.
*/ */
override fun run(sender: String, cmd: String, args: String, isPrivate: Boolean) { override fun run(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
with(bot) {
if (args.isNotBlank()) { if (args.isNotBlank()) {
try { try {
val results = searchGoogle( val results = searchGoogle(
@ -61,15 +66,14 @@ class GoogleSearch(bot: Mobibot) : ThreadedModule(bot) {
properties[GOOGLE_CSE_KEY_PROP] properties[GOOGLE_CSE_KEY_PROP]
) )
for (msg in results) { for (msg in results) {
send(sender, msg) event.sendMessage(channel, msg)
} }
} catch (e: ModuleException) { } catch (e: ModuleException) {
if (logger.isWarnEnabled) logger.warn(e.debugMessage, e) if (logger.isWarnEnabled) logger.warn(e.debugMessage, e)
send(sender, e.message, isPrivate) event.sendMessage(e.message!!)
} }
} else { } else {
helpResponse(sender, isPrivate) helpResponse(event)
}
} }
} }
@ -92,29 +96,33 @@ class GoogleSearch(bot: Mobibot) : ThreadedModule(bot) {
if (apiKey.isNullOrBlank() || cseKey.isNullOrBlank()) { if (apiKey.isNullOrBlank() || cseKey.isNullOrBlank()) {
throw ModuleException("${GOOGLE_CMD.capitalise()} is disabled. The API keys are missing.") throw ModuleException("${GOOGLE_CMD.capitalise()} is disabled. The API keys are missing.")
} }
return if (query.isNotBlank()) {
val results = mutableListOf<Message>() val results = mutableListOf<Message>()
if (query.isNotBlank()) {
try { try {
val url = URL( val url = URL(
"https://www.googleapis.com/customsearch/v1?key=$apiKey&cx=$cseKey" + "https://www.googleapis.com/customsearch/v1?key=$apiKey&cx=$cseKey" +
"&q=${encodeUrl(query)}&filter=1&num=5&alt=json" "&q=${encodeUrl(query)}&filter=1&num=5&alt=json"
) )
val json = JSONObject(urlReader(url)) val json = JSONObject(urlReader(url))
if (json.has("items")) {
val ja = json.getJSONArray("items") val ja = json.getJSONArray("items")
for (i in 0 until ja.length()) { for (i in 0 until ja.length()) {
val j = ja.getJSONObject(i) val j = ja.getJSONObject(i)
results.add(NoticeMessage(unescapeXml(j.getString("title")))) results.add(NoticeMessage(unescapeXml(j.getString("title"))))
results.add(NoticeMessage(helpFormat(j.getString("link"), false), Colors.DARK_GREEN)) results.add(NoticeMessage(helpFormat(j.getString("link"), false), Colors.DARK_GREEN))
} }
} else {
results.add(ErrorMessage("No results found.", Colors.RED))
}
} catch (e: IOException) { } catch (e: IOException) {
throw ModuleException("searchGoogle($query)", "An IO error has occurred searching Google.", e) throw ModuleException("searchGoogle($query)", "An IO error has occurred searching Google.", e)
} catch (e: JSONException) { } catch (e: JSONException) {
throw ModuleException("searchGoogle($query)", "A JSON error has occurred searching Google.", e) throw ModuleException("searchGoogle($query)", "A JSON error has occurred searching Google.", e)
} }
results
} else { } else {
throw ModuleException("Invalid query. Please try again.") results.add(ErrorMessage("Invalid query. Please try again."))
} }
return results
} }
} }

View file

@ -33,42 +33,43 @@ package net.thauvin.erik.mobibot.modules
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import net.thauvin.erik.mobibot.Mobibot import net.thauvin.erik.mobibot.Utils.bot
import net.thauvin.erik.mobibot.Utils.cyan import net.thauvin.erik.mobibot.Utils.cyan
import net.thauvin.erik.mobibot.Utils.helpFormat import net.thauvin.erik.mobibot.Utils.helpFormat
import net.thauvin.erik.mobibot.Utils.sendMessage
import net.thauvin.erik.mobibot.Utils.urlReader import net.thauvin.erik.mobibot.Utils.urlReader
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.json.JSONException import org.json.JSONException
import org.json.JSONObject import org.json.JSONObject
import org.pircbotx.hooks.types.GenericMessageEvent
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.io.IOException import java.io.IOException
import java.net.URL import java.net.URL
/** /**
* The Joke module. * The Joke module.
*/ */
class Joke(bot: Mobibot) : ThreadedModule(bot) { class Joke : ThreadedModule() {
override fun commandResponse( private val logger: Logger = LoggerFactory.getLogger(Joke::class.java)
sender: String,
cmd: String, override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
args: String,
isPrivate: Boolean
) {
runBlocking { runBlocking {
launch { run(sender, cmd, args, isPrivate) } launch { run(channel, cmd, args, event) }
} }
} }
/** /**
* Returns a random joke from [The Internet Chuck Norris Database](http://www.icndb.com/). * Returns a random joke from [The Internet Chuck Norris Database](http://www.icndb.com/).
*/ */
override fun run(sender: String, cmd: String, args: String, isPrivate: Boolean) { override fun run(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
with(bot) { with(event.bot()) {
try { try {
send(cyan(randomJoke().msg)) sendIRC().notice(channel, cyan(randomJoke().msg))
} catch (e: ModuleException) { } catch (e: ModuleException) {
if (logger.isWarnEnabled) logger.warn(e.debugMessage, e) if (logger.isWarnEnabled) logger.warn(e.debugMessage, e)
send(sender, e.message, isPrivate) event.sendMessage(e.message!!)
} }
} }
} }

View file

@ -32,9 +32,12 @@
package net.thauvin.erik.mobibot.modules package net.thauvin.erik.mobibot.modules
import net.thauvin.erik.mobibot.Constants import net.thauvin.erik.mobibot.Constants
import net.thauvin.erik.mobibot.Mobibot import net.thauvin.erik.mobibot.Utils.bot
import net.thauvin.erik.mobibot.Utils.helpFormat import net.thauvin.erik.mobibot.Utils.helpFormat
import org.apache.commons.net.whois.WhoisClient import org.apache.commons.net.whois.WhoisClient
import org.pircbotx.hooks.types.GenericMessageEvent
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.io.IOException import java.io.IOException
import java.net.InetAddress import java.net.InetAddress
import java.net.UnknownHostException import java.net.UnknownHostException
@ -42,19 +45,13 @@ import java.net.UnknownHostException
/** /**
* The Lookup module. * The Lookup module.
*/ */
class Lookup(bot: Mobibot) : AbstractModule(bot) { class Lookup : AbstractModule() {
override fun commandResponse( private val logger: Logger = LoggerFactory.getLogger(Lookup::class.java)
sender: String,
cmd: String, override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
args: String,
isPrivate: Boolean
) {
if (args.matches("(\\S.)+(\\S)+".toRegex())) { if (args.matches("(\\S.)+(\\S)+".toRegex())) {
with(bot) {
try { try {
nslookup(args).split(',').forEach { event.respondWith(nslookup(args).prependIndent())
send(it.trim())
}
} catch (ignore: UnknownHostException) { } catch (ignore: UnknownHostException) {
if (args.matches( if (args.matches(
("(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\." + ("(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\." +
@ -66,28 +63,33 @@ class Lookup(bot: Mobibot) : AbstractModule(bot) {
val lines = whois(args) val lines = whois(args)
if (lines.isNotEmpty()) { if (lines.isNotEmpty()) {
var line: String var line: String
var hasData = false
for (rawLine in lines) { for (rawLine in lines) {
line = rawLine.trim() line = rawLine.trim()
if (line.isNotEmpty() && line[0] != '#') { if (line.matches("^\\b(?!\\b[Cc]omment\\b)\\w+\\b: .*$".toRegex())) {
send(line) if (!hasData) {
event.respondWith(line)
hasData = true
} else {
event.bot().sendIRC().notice(event.user.nick, line)
}
} }
} }
} else { } else {
send("Unknown host.") event.respond("Unknown host.")
} }
} catch (ioe: IOException) { } catch (ioe: IOException) {
if (logger.isDebugEnabled) { if (logger.isWarnEnabled) {
logger.debug("Unable to perform whois IP lookup: $args", ioe) logger.warn("Unable to perform whois IP lookup: $args", ioe)
} }
send("Unable to perform whois IP lookup: ${ioe.message}") event.respond("Unable to perform whois IP lookup: ${ioe.message}")
} }
} else { } else {
send("Unknown host.") event.respond("Unknown host.")
}
} }
} }
} else { } else {
helpResponse(sender, true) helpResponse(event)
} }
} }

View file

@ -31,24 +31,20 @@
*/ */
package net.thauvin.erik.mobibot.modules package net.thauvin.erik.mobibot.modules
import net.thauvin.erik.mobibot.Mobibot import net.thauvin.erik.mobibot.Utils.bot
import net.thauvin.erik.mobibot.Utils.helpFormat import net.thauvin.erik.mobibot.Utils.helpFormat
import org.pircbotx.hooks.types.GenericMessageEvent
import kotlin.random.Random import kotlin.random.Random
/** /**
* The Ping module. * The Ping module.
*/ */
class Ping(bot: Mobibot) : AbstractModule(bot) { class Ping : AbstractModule() {
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */
override fun commandResponse( override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
sender: String, event.bot().sendIRC().action(channel, randomPing())
cmd: String,
args: String,
isPrivate: Boolean
) {
bot.action(randomPing())
} }
companion object { companion object {

View file

@ -32,16 +32,17 @@
package net.thauvin.erik.mobibot.modules package net.thauvin.erik.mobibot.modules
import net.thauvin.erik.mobibot.Mobibot import net.thauvin.erik.mobibot.Utils.bot
import net.thauvin.erik.mobibot.Utils.capitalise import net.thauvin.erik.mobibot.Utils.capitalise
import net.thauvin.erik.mobibot.Utils.helpFormat import net.thauvin.erik.mobibot.Utils.helpFormat
import org.pircbotx.hooks.types.GenericMessageEvent
import kotlin.random.Random import kotlin.random.Random
/** /**
* Simple module example in Kotlin. * Simple module example in Kotlin.
*/ */
class RockPaperScissors(bot: Mobibot) : AbstractModule(bot) { class RockPaperScissors : AbstractModule() {
init { init {
with(commands) { with(commands) {
add(Hands.ROCK.name.lowercase()) add(Hands.ROCK.name.lowercase())
@ -101,20 +102,26 @@ class RockPaperScissors(bot: Mobibot) : AbstractModule(bot) {
} }
} }
override fun commandResponse(sender: String, cmd: String, args: String, isPrivate: Boolean) { 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.values()[Random.nextInt(0, Hands.values().size)] val botHand = Hands.values()[Random.nextInt(0, Hands.values().size)]
with(bot) { with(event.bot()) {
send("${hand.emoji} vs. ${botHand.emoji}") sendIRC().message(channel, "${hand.emoji} vs. ${botHand.emoji}")
when { when {
hand == botHand -> { hand == botHand -> {
action("tied.") sendIRC().action(channel, "tied.")
} }
hand.beats(botHand) -> { hand.beats(botHand) -> {
action("lost. ${hand.name.capitalise()} ${hand.action} ${botHand.name.lowercase()}.") sendIRC().action(
channel,
"lost. ${hand.name.capitalise()} ${hand.action} ${botHand.name.lowercase()}."
)
} }
else -> { else -> {
action("wins. ${botHand.name.capitalise()} ${botHand.action} ${hand.name.lowercase()}.") sendIRC().action(
channel,
"wins. ${botHand.name.capitalise()} ${botHand.action} ${hand.name.lowercase()}."
)
} }
} }
} }

View file

@ -31,10 +31,10 @@
*/ */
package net.thauvin.erik.mobibot.modules package net.thauvin.erik.mobibot.modules
import net.thauvin.erik.mobibot.Mobibot
import net.thauvin.erik.mobibot.Utils.capitalise 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.sendMessage
import net.thauvin.erik.mobibot.Utils.unescapeXml import net.thauvin.erik.mobibot.Utils.unescapeXml
import net.thauvin.erik.mobibot.Utils.urlReader import net.thauvin.erik.mobibot.Utils.urlReader
import net.thauvin.erik.mobibot.msg.ErrorMessage import net.thauvin.erik.mobibot.msg.ErrorMessage
@ -43,31 +43,34 @@ import net.thauvin.erik.mobibot.msg.NoticeMessage
import net.thauvin.erik.mobibot.msg.PublicMessage import net.thauvin.erik.mobibot.msg.PublicMessage
import org.json.JSONException import org.json.JSONException
import org.json.JSONObject import org.json.JSONObject
import org.pircbotx.hooks.types.GenericMessageEvent
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.io.IOException import java.io.IOException
import java.net.URL import java.net.URL
/** /**
* The StockQuote module. * The StockQuote module.
*/ */
class StockQuote(bot: Mobibot) : ThreadedModule(bot) { class StockQuote : ThreadedModule() {
private val logger: Logger = LoggerFactory.getLogger(StockQuote::class.java)
/** /**
* Returns the specified stock quote from Alpha Vantage. * Returns the specified stock quote from Alpha Vantage.
*/ */
override fun run(sender: String, cmd: String, args: String, isPrivate: Boolean) { override fun run(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
with(bot) {
if (args.isNotBlank()) { if (args.isNotBlank()) {
try { try {
val messages = getQuote(args, properties[ALPHAVANTAGE_API_KEY_PROP]) val messages = getQuote(args, properties[ALPHAVANTAGE_API_KEY_PROP])
for (msg in messages) { for (msg in messages) {
send(sender, msg) event.sendMessage(channel, msg)
} }
} catch (e: ModuleException) { } catch (e: ModuleException) {
if (logger.isWarnEnabled) logger.warn(e.debugMessage, e) if (logger.isWarnEnabled) logger.warn(e.debugMessage, e)
send(e.message) event.sendMessage(e.message!!)
} }
} else { } else {
helpResponse(sender, isPrivate) helpResponse(event)
}
} }
} }
@ -129,9 +132,9 @@ class StockQuote(bot: Mobibot) : ThreadedModule(bot) {
"${STOCK_CMD.capitalise()} is disabled. The API key is missing." "${STOCK_CMD.capitalise()} is disabled. The API key is missing."
) )
} }
return if (symbol.isNotBlank()) {
val debugMessage = "getQuote($symbol)"
val messages = mutableListOf<Message>() val messages = mutableListOf<Message>()
if (symbol.isNotBlank()) {
val debugMessage = "getQuote($symbol)"
var response: String var response: String
try { try {
with(messages) { with(messages) {
@ -161,8 +164,7 @@ class StockQuote(bot: Mobibot) : ThreadedModule(bot) {
val quote = json.getJSONObject("Global Quote") val quote = json.getJSONObject("Global Quote")
if (quote.isEmpty) { if (quote.isEmpty) {
add(ErrorMessage(INVALID_SYMBOL)) add(ErrorMessage(INVALID_SYMBOL))
return messages } else {
}
add( add(
PublicMessage( PublicMessage(
@ -171,7 +173,6 @@ class StockQuote(bot: Mobibot) : ThreadedModule(bot) {
) )
) )
@Suppress("MagicNumber")
val pad = 10 val pad = 10
add( add(
@ -213,15 +214,16 @@ class StockQuote(bot: Mobibot) : ThreadedModule(bot) {
) )
} }
} }
}
} catch (e: IOException) { } catch (e: IOException) {
throw ModuleException(debugMessage, "An IO error has occurred retrieving a stock quote.", e) throw ModuleException(debugMessage, "An IO error has occurred retrieving a stock quote.", e)
} catch (e: NullPointerException) { } catch (e: NullPointerException) {
throw ModuleException(debugMessage, "An error has occurred retrieving a stock quote.", e) throw ModuleException(debugMessage, "An error has occurred retrieving a stock quote.", e)
} }
messages
} else { } else {
throw ModuleException(INVALID_SYMBOL) messages.add(ErrorMessage(INVALID_SYMBOL))
} }
return messages
} }
} }

View file

@ -33,36 +33,26 @@ package net.thauvin.erik.mobibot.modules
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import net.thauvin.erik.mobibot.Mobibot import org.pircbotx.hooks.types.GenericMessageEvent
/** /**
* The `ThreadedModule` class. * The `ThreadedModule` class.
*/ */
abstract class ThreadedModule(bot: Mobibot) : AbstractModule(bot) { abstract class ThreadedModule : AbstractModule() {
override fun commandResponse( override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
sender: String, if (isEnabled && event.message.isNotEmpty()) {
cmd: String,
args: String,
isPrivate: Boolean
) {
if (isEnabled && args.isNotEmpty()) {
runBlocking { runBlocking {
launch { launch {
run(sender, cmd, args, isPrivate) run(channel, cmd, args, event)
} }
} }
} else { } else {
helpResponse(sender, isPrivate) helpResponse(event)
} }
} }
/** /**
* Runs the thread. * Runs the thread.
*/ */
abstract fun run( abstract fun run(channel: String, cmd: String, args: String, event: GenericMessageEvent)
sender: String,
cmd: String,
args: String,
isPrivate: Boolean
)
} }

View file

@ -34,21 +34,26 @@ package net.thauvin.erik.mobibot.modules
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import net.thauvin.erik.mobibot.Constants import net.thauvin.erik.mobibot.Constants
import net.thauvin.erik.mobibot.Mobibot
import net.thauvin.erik.mobibot.TwitterTimer import net.thauvin.erik.mobibot.TwitterTimer
import net.thauvin.erik.mobibot.Utils.helpFormat import net.thauvin.erik.mobibot.Utils.helpFormat
import net.thauvin.erik.mobibot.commands.links.LinksMgr import net.thauvin.erik.mobibot.commands.links.LinksMgr
import net.thauvin.erik.mobibot.entries.EntriesUtils import net.thauvin.erik.mobibot.entries.EntriesUtils
import net.thauvin.erik.mobibot.msg.Message import org.pircbotx.hooks.types.GenericMessageEvent
import net.thauvin.erik.mobibot.msg.NoticeMessage import org.slf4j.Logger
import org.slf4j.LoggerFactory
import twitter4j.TwitterException import twitter4j.TwitterException
import twitter4j.TwitterFactory import twitter4j.TwitterFactory
import twitter4j.conf.ConfigurationBuilder import twitter4j.conf.ConfigurationBuilder
import java.util.Timer
/** /**
* The Twitter module. * The Twitter module.
*/ */
class Twitter(bot: Mobibot) : ThreadedModule(bot) { class Twitter : ThreadedModule() {
private val logger: Logger = LoggerFactory.getLogger(Twitter::class.java)
private val timer = Timer(true)
// Twitter auto-posts. // Twitter auto-posts.
private val entries: MutableSet<Int> = HashSet() private val entries: MutableSet<Int> = HashSet()
@ -87,7 +92,6 @@ class Twitter(bot: Mobibot) : ThreadedModule(bot) {
* Send a notification to the registered Twitter handle. * Send a notification to the registered Twitter handle.
*/ */
fun notification(msg: String) { fun notification(msg: String) {
with(bot) {
if (isEnabled && !handle.isNullOrBlank()) { if (isEnabled && !handle.isNullOrBlank()) {
runBlocking { runBlocking {
launch { launch {
@ -101,13 +105,12 @@ class Twitter(bot: Mobibot) : ThreadedModule(bot) {
} }
} }
} }
}
/** /**
* Posts on Twitter. * Posts on Twitter.
*/ */
@Throws(ModuleException::class) @Throws(ModuleException::class)
fun post(handle: String = "${properties[HANDLE_PROP]}", message: String, isDm: Boolean): Message { fun post(handle: String = "${properties[HANDLE_PROP]}", message: String, isDm: Boolean): String {
return twitterPost( return twitterPost(
properties[CONSUMER_KEY_PROP], properties[CONSUMER_KEY_PROP],
properties[CONSUMER_SECRET_PROP], properties[CONSUMER_SECRET_PROP],
@ -123,15 +126,14 @@ class Twitter(bot: Mobibot) : ThreadedModule(bot) {
* Post an entry to twitter. * Post an entry to twitter.
*/ */
fun postEntry(index: Int) { fun postEntry(index: Int) {
with(bot) { if (isAutoPost && hasEntry(index) && LinksMgr.entries.links.size >= index) {
if (isAutoPost && hasEntry(index) && LinksMgr.entries.size >= index) { val entry = LinksMgr.entries.links[index]
val entry = LinksMgr.entries[index] val msg = "${entry.title} ${entry.link} via ${entry.nick} on ${entry.channel}"
val msg = "${entry.title} ${entry.link} via ${entry.nick} on $channel"
runBlocking { runBlocking {
launch { launch {
try { try {
if (logger.isDebugEnabled) { if (logger.isDebugEnabled) {
logger.debug("Posting {} to Twitter.", EntriesUtils.buildLinkCmd(index)) logger.debug("Posting {} to Twitter.", EntriesUtils.buildLinkLabel(index))
} }
post(message = msg, isDm = false) post(message = msg, isDm = false)
} catch (e: ModuleException) { } catch (e: ModuleException) {
@ -142,16 +144,14 @@ class Twitter(bot: Mobibot) : ThreadedModule(bot) {
removeEntry(index) removeEntry(index)
} }
} }
}
fun queueEntry(index: Int) { fun queueEntry(index: Int) {
if (isAutoPost) { if (isAutoPost) {
addEntry(index) addEntry(index)
if (bot.logger.isDebugEnabled) { if (logger.isDebugEnabled) {
bot.logger.debug("Scheduling {} for posting on Twitter.", EntriesUtils.buildLinkCmd(index)) logger.debug("Scheduling {} for posting on Twitter.", EntriesUtils.buildLinkLabel(index))
} }
@Suppress("MagicNumber") timer.schedule(TwitterTimer(this, index), Constants.TIMER_DELAY * 60L * 1000L)
bot.timer.schedule(TwitterTimer(bot, index), Constants.TIMER_DELAY * 60L * 1000L)
} }
} }
@ -162,18 +162,12 @@ class Twitter(bot: Mobibot) : ThreadedModule(bot) {
/** /**
* Posts to twitter. * Posts to twitter.
*/ */
override fun run(sender: String, cmd: String, args: String, isPrivate: Boolean) { override fun run(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
with(bot) {
try { try {
send( event.respond(post(event.user.nick, "$args (by ${event.user.nick} on $channel)", false))
sender,
post(sender, "$args (by $sender on $channel)", false).msg,
isPrivate
)
} catch (e: ModuleException) { } catch (e: ModuleException) {
if (logger.isWarnEnabled) logger.warn(e.debugMessage, e) if (logger.isWarnEnabled) logger.warn(e.debugMessage, e)
send(sender, e.message, isPrivate) event.respond(e.message)
}
} }
} }
@ -181,6 +175,7 @@ class Twitter(bot: Mobibot) : ThreadedModule(bot) {
* Post all the entries to Twitter on shutdown. * Post all the entries to Twitter on shutdown.
*/ */
fun shutdown() { fun shutdown() {
timer.cancel()
if (isAutoPost) { if (isAutoPost) {
for (index in entries) { for (index in entries) {
postEntry(index) postEntry(index)
@ -213,23 +208,23 @@ class Twitter(bot: Mobibot) : ThreadedModule(bot) {
handle: String?, handle: String?,
message: String, message: String,
isDm: Boolean isDm: Boolean
): Message { ): String {
return try { return try {
val cb = ConfigurationBuilder() val cb = ConfigurationBuilder().apply {
cb.setDebugEnabled(true) setDebugEnabled(true)
.setOAuthConsumerKey(consumerKey) setOAuthConsumerKey(consumerKey)
.setOAuthConsumerSecret(consumerSecret) setOAuthConsumerSecret(consumerSecret)
.setOAuthAccessToken(token).setOAuthAccessTokenSecret(tokenSecret) setOAuthAccessToken(token)
setOAuthAccessTokenSecret(tokenSecret)
}
val tf = TwitterFactory(cb.build()) val tf = TwitterFactory(cb.build())
val twitter = tf.instance val twitter = tf.instance
if (!isDm) { if (!isDm) {
val status = twitter.updateStatus(message) val status = twitter.updateStatus(message)
NoticeMessage( "Your message was posted to https://twitter.com/${twitter.screenName}/statuses/${status.id}"
"You message was posted to https://twitter.com/${twitter.screenName}/statuses/${status.id}"
)
} else { } else {
val dm = twitter.sendDirectMessage(handle, message) val dm = twitter.sendDirectMessage(handle, message)
NoticeMessage(dm.text) dm.text
} }
} catch (e: TwitterException) { } catch (e: TwitterException) {
throw ModuleException("twitterPost($message)", "An error has occurred: ${e.message}", e) throw ModuleException("twitterPost($message)", "An error has occurred: ${e.message}", e)

View file

@ -35,45 +35,48 @@ import net.aksingh.owmjapis.api.APIException
import net.aksingh.owmjapis.core.OWM 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.Mobibot
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.capitalise
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
import net.thauvin.erik.mobibot.Utils.sendMessage
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.NoticeMessage import net.thauvin.erik.mobibot.msg.NoticeMessage
import net.thauvin.erik.mobibot.msg.PublicMessage import net.thauvin.erik.mobibot.msg.PublicMessage
import org.jibble.pircbot.Colors import org.pircbotx.Colors
import org.pircbotx.hooks.types.GenericMessageEvent
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import kotlin.math.roundToInt import kotlin.math.roundToInt
/** /**
* The `Weather2` module. * The `Weather2` module.
*/ */
class Weather2(bot: Mobibot) : ThreadedModule(bot) { class Weather2 : ThreadedModule() {
private val logger: Logger = LoggerFactory.getLogger(Weather2::class.java)
/** /**
* Fetches the weather data from a specific city. * Fetches the weather data from a specific city.
*/ */
override fun run(sender: String, cmd: String, args: String, isPrivate: Boolean) { override fun run(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
if (args.isNotBlank()) { if (args.isNotBlank()) {
with(bot) {
try { try {
val messages = getWeather(args, properties[OWM_API_KEY_PROP]) val messages = getWeather(args, properties[OWM_API_KEY_PROP])
if (messages[0].isError) { if (messages[0].isError) {
helpResponse(sender, isPrivate) helpResponse(event)
} else { } else {
for (msg in messages) { for (msg in messages) {
send(sender, msg) event.sendMessage(channel, msg)
} }
} }
} catch (e: ModuleException) { } catch (e: ModuleException) {
if (logger.isDebugEnabled) logger.debug(e.debugMessage, e) if (logger.isWarnEnabled) logger.warn(e.debugMessage, e)
send(e.message) event.respond(e.message)
}
} }
} else { } else {
helpResponse(sender, isPrivate) helpResponse(event)
} }
} }
@ -90,7 +93,6 @@ class Weather2(bot: Mobibot) : ThreadedModule(bot) {
* Converts and rounds temperature from °F to °C. * Converts and rounds temperature from °F to °C.
*/ */
fun ftoC(d: Double?): Pair<Int, Int> { fun ftoC(d: Double?): Pair<Int, Int> {
@Suppress("MagicNumber")
val c = (d!! - 32) * 5 / 9 val c = (d!! - 32) * 5 / 9
return d.roundToInt() to c.roundToInt() return d.roundToInt() to c.roundToInt()
} }
@ -208,7 +210,6 @@ class Weather2(bot: Mobibot) : ThreadedModule(bot) {
* Converts and rounds temperature from mph to km/h. * Converts and rounds temperature from mph to km/h.
*/ */
fun mphToKmh(w: Double): Pair<Int, Int> { fun mphToKmh(w: Double): Pair<Int, Int> {
@Suppress("MagicNumber")
val kmh = w * 1.60934 val kmh = w * 1.60934
return w.roundToInt() to kmh.roundToInt() return w.roundToInt() to kmh.roundToInt()
} }

View file

@ -31,12 +31,11 @@
*/ */
package net.thauvin.erik.mobibot.modules package net.thauvin.erik.mobibot.modules
import net.thauvin.erik.mobibot.Mobibot
import net.thauvin.erik.mobibot.Utils.bold import net.thauvin.erik.mobibot.Utils.bold
import net.thauvin.erik.mobibot.Utils.helpFormat import net.thauvin.erik.mobibot.Utils.helpFormat
import net.thauvin.erik.mobibot.msg.ErrorMessage import net.thauvin.erik.mobibot.Utils.sendList
import net.thauvin.erik.mobibot.msg.Message import net.thauvin.erik.mobibot.Utils.sendMessage
import net.thauvin.erik.mobibot.msg.PublicMessage import org.pircbotx.hooks.types.GenericMessageEvent
import java.time.ZoneId import java.time.ZoneId
import java.time.ZonedDateTime import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
@ -46,7 +45,7 @@ import java.util.Collections
/** /**
* The WorldTime module. * The WorldTime module.
*/ */
class WorldTime(bot: Mobibot) : AbstractModule(bot) { class WorldTime : AbstractModule() {
companion object { companion object {
// Beats (Internet Time) keyword // Beats (Internet Time) keyword
const val BEATS_KEYWORD = ".beats" const val BEATS_KEYWORD = ".beats"
@ -57,6 +56,12 @@ class WorldTime(bot: Mobibot) : AbstractModule(bot) {
// The Time command // The Time command
private const val TIME_CMD = "time" private const val TIME_CMD = "time"
// The zones arguments
private const val ZONES_ARGS = "zones"
// The default zone
private const val DEFAULT_ZONE = "PST"
// Date/Time Format // Date/Time Format
private var dtf = private var dtf =
DateTimeFormatter.ofPattern("'The time is ${bold("'HH:mm'")} on ${bold("'EEEE, d MMMM yyyy'")} in '") DateTimeFormatter.ofPattern("'The time is ${bold("'HH:mm'")} on ${bold("'EEEE, d MMMM yyyy'")} in '")
@ -64,342 +69,316 @@ class WorldTime(bot: Mobibot) : AbstractModule(bot) {
/** /**
* Returns the current Internet (beat) Time. * Returns the current Internet (beat) Time.
*/ */
@Suppress("MagicNumber", "ImplicitDefaultLocale")
private fun internetTime(): String { private fun internetTime(): String {
val zdt = ZonedDateTime.now(ZoneId.of("UTC+01:00")) val zdt = ZonedDateTime.now(ZoneId.of("UTC+01:00"))
val beats = ((zdt[ChronoField.SECOND_OF_MINUTE] + zdt[ChronoField.MINUTE_OF_HOUR] * 60 val beats = ((zdt[ChronoField.SECOND_OF_MINUTE] + zdt[ChronoField.MINUTE_OF_HOUR] * 60
+ zdt[ChronoField.HOUR_OF_DAY] * 3600) / 86.4).toInt() + zdt[ChronoField.HOUR_OF_DAY] * 3600) / 86.4).toInt()
return String.format("%c%03d", '@', beats) return "%c%03d".format('@', beats)
} }
/** /**
* Returns the time for the given timezone/city. * Returns the time for the given timezone/city.
*/ */
@JvmStatic @JvmStatic
fun time(query: String): Message { fun time(query: String = DEFAULT_ZONE): String {
val tz = COUNTRIES_MAP[(query.substring(query.indexOf(' ') + 1).trim()).uppercase()] val tz = COUNTRIES_MAP[(if (query.isNotBlank()) query.trim().uppercase() else DEFAULT_ZONE)]
val response: String = if (tz != null) { return if (tz != null) {
if (BEATS_KEYWORD == tz) { if (BEATS_KEYWORD == tz) {
"The current Internet Time is: ${bold(internetTime())} $BEATS_KEYWORD" "The current Internet Time is ${bold(internetTime())} $BEATS_KEYWORD"
} else { } else {
(ZonedDateTime.now().withZoneSameInstant(ZoneId.of(tz)).format(dtf) (ZonedDateTime.now().withZoneSameInstant(ZoneId.of(tz)).format(dtf)
+ bold(tz.substring(tz.lastIndexOf('/') + 1).replace('_', ' '))) + bold(tz.substring(tz.lastIndexOf('/') + 1).replace('_', ' ')))
} }
} else { } else {
return ErrorMessage("Unsupported country/zone. Please try again.") "Unsupported country/zone. Please try again."
} }
return PublicMessage(response)
} }
init { init {
// Initialize the countries map // Initialize the zones map
val countries = mutableMapOf<String, String>() val zones = mutableMapOf<String, String>()
countries["AD"] = "Europe/Andorra" zones["AD"] = "Europe/Andorra"
countries["AE"] = "Asia/Dubai" zones["AE"] = "Asia/Dubai"
countries["AF"] = "Asia/Kabul" zones["AF"] = "Asia/Kabul"
countries["AG"] = "America/Antigua" zones["AG"] = "America/Antigua"
countries["AI"] = "America/Anguilla" zones["AI"] = "America/Anguilla"
countries["AKDT"] = "America/Anchorage" zones["AKDT"] = "America/Anchorage"
countries["AKST"] = "America/Anchorage" zones["AKST"] = "America/Anchorage"
countries["AL"] = "Europe/Tirane" zones["AL"] = "Europe/Tirane"
countries["AM"] = "Asia/Yerevan" zones["AM"] = "Asia/Yerevan"
countries["AO"] = "Africa/Luanda" zones["AO"] = "Africa/Luanda"
countries["AQ"] = "Antarctica/South_Pole" zones["AQ"] = "Antarctica/South_Pole"
countries["AR"] = "America/Argentina/Buenos_Aires" zones["AR"] = "America/Argentina/Buenos_Aires"
countries["AS"] = "Pacific/Pago_Pago" zones["AS"] = "Pacific/Pago_Pago"
countries["AT"] = "Europe/Vienna" zones["AT"] = "Europe/Vienna"
countries["AU"] = "Australia/Sydney" zones["AU"] = "Australia/Sydney"
countries["AW"] = "America/Aruba" zones["AW"] = "America/Aruba"
countries["AX"] = "Europe/Mariehamn" zones["AX"] = "Europe/Mariehamn"
countries["AZ"] = "Asia/Baku" zones["AZ"] = "Asia/Baku"
countries["BA"] = "Europe/Sarajevo" zones["BA"] = "Europe/Sarajevo"
countries["BB"] = "America/Barbados" zones["BB"] = "America/Barbados"
countries["BD"] = "Asia/Dhaka" zones["BD"] = "Asia/Dhaka"
countries["BE"] = "Europe/Brussels" zones["BE"] = "Europe/Brussels"
countries["BEAT"] = BEATS_KEYWORD zones["BEAT"] = BEATS_KEYWORD
countries["BF"] = "Africa/Ouagadougou" zones["BF"] = "Africa/Ouagadougou"
countries["BG"] = "Europe/Sofia" zones["BG"] = "Europe/Sofia"
countries["BH"] = "Asia/Bahrain" zones["BH"] = "Asia/Bahrain"
countries["BI"] = "Africa/Bujumbura" zones["BI"] = "Africa/Bujumbura"
countries["BJ"] = "Africa/Porto-Novo" zones["BJ"] = "Africa/Porto-Novo"
countries["BL"] = "America/St_Barthelemy" zones["BL"] = "America/St_Barthelemy"
countries["BM"] = "Atlantic/Bermuda" zones["BM"] = "Atlantic/Bermuda"
countries["BMT"] = BEATS_KEYWORD zones["BMT"] = BEATS_KEYWORD
countries["BN"] = "Asia/Brunei" zones["BN"] = "Asia/Brunei"
countries["BO"] = "America/La_Paz" zones["BO"] = "America/La_Paz"
countries["BQ"] = "America/Kralendijk" zones["BQ"] = "America/Kralendijk"
countries["BR"] = "America/Sao_Paulo" zones["BR"] = "America/Sao_Paulo"
countries["BS"] = "America/Nassau" zones["BS"] = "America/Nassau"
countries["BT"] = "Asia/Thimphu" zones["BT"] = "Asia/Thimphu"
countries["BW"] = "Africa/Gaborone" zones["BW"] = "Africa/Gaborone"
countries["BY"] = "Europe/Minsk" zones["BY"] = "Europe/Minsk"
countries["BZ"] = "America/Belize" zones["BZ"] = "America/Belize"
countries["CA"] = "America/Montreal" zones["CA"] = "America/Montreal"
countries["CC"] = "Indian/Cocos" zones["CC"] = "Indian/Cocos"
countries["CD"] = "Africa/Kinshasa" zones["CD"] = "Africa/Kinshasa"
countries["CDT"] = "America/Chicago" zones["CDT"] = "America/Chicago"
countries["CET"] = "CET" zones["CET"] = "CET"
countries["CF"] = "Africa/Bangui" zones["CF"] = "Africa/Bangui"
countries["CG"] = "Africa/Brazzaville" zones["CG"] = "Africa/Brazzaville"
countries["CH"] = "Europe/Zurich" zones["CH"] = "Europe/Zurich"
countries["CI"] = "Africa/Abidjan" zones["CI"] = "Africa/Abidjan"
countries["CK"] = "Pacific/Rarotonga" zones["CK"] = "Pacific/Rarotonga"
countries["CL"] = "America/Santiago" zones["CL"] = "America/Santiago"
countries["CM"] = "Africa/Douala" zones["CM"] = "Africa/Douala"
countries["CN"] = "Asia/Shanghai" zones["CN"] = "Asia/Shanghai"
countries["CO"] = "America/Bogota" zones["CO"] = "America/Bogota"
countries["CR"] = "America/Costa_Rica" zones["CR"] = "America/Costa_Rica"
countries["CST"] = "America/Chicago" zones["CST"] = "America/Chicago"
countries["CU"] = "Cuba" zones["CU"] = "Cuba"
countries["CV"] = "Atlantic/Cape_Verde" zones["CV"] = "Atlantic/Cape_Verde"
countries["CW"] = "America/Curacao" zones["CW"] = "America/Curacao"
countries["CX"] = "Indian/Christmas" zones["CX"] = "Indian/Christmas"
countries["CY"] = "Asia/Nicosia" zones["CY"] = "Asia/Nicosia"
countries["CZ"] = "Europe/Prague" zones["CZ"] = "Europe/Prague"
countries["DE"] = "Europe/Berlin" zones["DE"] = "Europe/Berlin"
countries["DJ"] = "Africa/Djibouti" zones["DJ"] = "Africa/Djibouti"
countries["DK"] = "Europe/Copenhagen" zones["DK"] = "Europe/Copenhagen"
countries["DM"] = "America/Dominica" zones["DM"] = "America/Dominica"
countries["DO"] = "America/Santo_Domingo" zones["DO"] = "America/Santo_Domingo"
countries["DZ"] = "Africa/Algiers" zones["DZ"] = "Africa/Algiers"
countries["EC"] = "Pacific/Galapagos" zones["EC"] = "Pacific/Galapagos"
countries["EDT"] = "America/New_York" zones["EDT"] = "America/New_York"
countries["EE"] = "Europe/Tallinn" zones["EE"] = "Europe/Tallinn"
countries["EG"] = "Africa/Cairo" zones["EG"] = "Africa/Cairo"
countries["EH"] = "Africa/El_Aaiun" zones["EH"] = "Africa/El_Aaiun"
countries["ER"] = "Africa/Asmara" zones["ER"] = "Africa/Asmara"
countries["ES"] = "Europe/Madrid" zones["ES"] = "Europe/Madrid"
countries["EST"] = "America/New_York" zones["EST"] = "America/New_York"
countries["ET"] = "Africa/Addis_Ababa" zones["ET"] = "Africa/Addis_Ababa"
countries["FI"] = "Europe/Helsinki" zones["FI"] = "Europe/Helsinki"
countries["FJ"] = "Pacific/Fiji" zones["FJ"] = "Pacific/Fiji"
countries["FK"] = "Atlantic/Stanley" zones["FK"] = "Atlantic/Stanley"
countries["FM"] = "Pacific/Yap" zones["FM"] = "Pacific/Yap"
countries["FO"] = "Atlantic/Faroe" zones["FO"] = "Atlantic/Faroe"
countries["FR"] = "Europe/Paris" zones["FR"] = "Europe/Paris"
countries["GA"] = "Africa/Libreville" zones["GA"] = "Africa/Libreville"
countries["GB"] = "Europe/London" zones["GB"] = "Europe/London"
countries["GD"] = "America/Grenada" zones["GD"] = "America/Grenada"
countries["GE"] = "Asia/Tbilisi" zones["GE"] = "Asia/Tbilisi"
countries["GF"] = "America/Cayenne" zones["GF"] = "America/Cayenne"
countries["GG"] = "Europe/Guernsey" zones["GG"] = "Europe/Guernsey"
countries["GH"] = "Africa/Accra" zones["GH"] = "Africa/Accra"
countries["GI"] = "Europe/Gibraltar" zones["GI"] = "Europe/Gibraltar"
countries["GL"] = "America/Thule" zones["GL"] = "America/Thule"
countries["GM"] = "Africa/Banjul" zones["GM"] = "Africa/Banjul"
countries["GMT"] = "GMT" zones["GMT"] = "GMT"
countries["GN"] = "Africa/Conakry" zones["GN"] = "Africa/Conakry"
countries["GP"] = "America/Guadeloupe" zones["GP"] = "America/Guadeloupe"
countries["GQ"] = "Africa/Malabo" zones["GQ"] = "Africa/Malabo"
countries["GR"] = "Europe/Athens" zones["GR"] = "Europe/Athens"
countries["GS"] = "Atlantic/South_Georgia" zones["GS"] = "Atlantic/South_Georgia"
countries["GT"] = "America/Guatemala" zones["GT"] = "America/Guatemala"
countries["GU"] = "Pacific/Guam" zones["GU"] = "Pacific/Guam"
countries["GW"] = "Africa/Bissau" zones["GW"] = "Africa/Bissau"
countries["GY"] = "America/Guyana" zones["GY"] = "America/Guyana"
countries["HK"] = "Asia/Hong_Kong" zones["HK"] = "Asia/Hong_Kong"
countries["HN"] = "America/Tegucigalpa" zones["HN"] = "America/Tegucigalpa"
countries["HR"] = "Europe/Zagreb" zones["HR"] = "Europe/Zagreb"
countries["HST"] = "Pacific/Honolulu" zones["HST"] = "Pacific/Honolulu"
countries["HT"] = "America/Port-au-Prince" zones["HT"] = "America/Port-au-Prince"
countries["HU"] = "Europe/Budapest" zones["HU"] = "Europe/Budapest"
countries["ID"] = "Asia/Jakarta" zones["ID"] = "Asia/Jakarta"
countries["IE"] = "Europe/Dublin" zones["IE"] = "Europe/Dublin"
countries["IL"] = "Asia/Tel_Aviv" zones["IL"] = "Asia/Tel_Aviv"
countries["IM"] = "Europe/Isle_of_Man" zones["IM"] = "Europe/Isle_of_Man"
countries["IN"] = "Asia/Kolkata" zones["IN"] = "Asia/Kolkata"
countries["IO"] = "Indian/Chagos" zones["IO"] = "Indian/Chagos"
countries["IQ"] = "Asia/Baghdad" zones["IQ"] = "Asia/Baghdad"
countries["IR"] = "Asia/Tehran" zones["IR"] = "Asia/Tehran"
countries["IS"] = "Atlantic/Reykjavik" zones["IS"] = "Atlantic/Reykjavik"
countries["IT"] = "Europe/Rome" zones["IT"] = "Europe/Rome"
countries["JE"] = "Europe/Jersey" zones["JE"] = "Europe/Jersey"
countries["JM"] = "Jamaica" zones["JM"] = "Jamaica"
countries["JO"] = "Asia/Amman" zones["JO"] = "Asia/Amman"
countries["JP"] = "Asia/Tokyo" zones["JP"] = "Asia/Tokyo"
countries["KE"] = "Africa/Nairobi" zones["KE"] = "Africa/Nairobi"
countries["KG"] = "Asia/Bishkek" zones["KG"] = "Asia/Bishkek"
countries["KH"] = "Asia/Phnom_Penh" zones["KH"] = "Asia/Phnom_Penh"
countries["KI"] = "Pacific/Tarawa" zones["KI"] = "Pacific/Tarawa"
countries["KM"] = "Indian/Comoro" zones["KM"] = "Indian/Comoro"
countries["KN"] = "America/St_Kitts" zones["KN"] = "America/St_Kitts"
countries["KP"] = "Asia/Pyongyang" zones["KP"] = "Asia/Pyongyang"
countries["KR"] = "Asia/Seoul" zones["KR"] = "Asia/Seoul"
countries["KW"] = "Asia/Riyadh" zones["KW"] = "Asia/Riyadh"
countries["KY"] = "America/Cayman" zones["KY"] = "America/Cayman"
countries["KZ"] = "Asia/Oral" zones["KZ"] = "Asia/Oral"
countries["LA"] = "Asia/Vientiane" zones["LA"] = "Asia/Vientiane"
countries["LB"] = "Asia/Beirut" zones["LB"] = "Asia/Beirut"
countries["LC"] = "America/St_Lucia" zones["LC"] = "America/St_Lucia"
countries["LI"] = "Europe/Vaduz" zones["LI"] = "Europe/Vaduz"
countries["LK"] = "Asia/Colombo" zones["LK"] = "Asia/Colombo"
countries["LR"] = "Africa/Monrovia" zones["LR"] = "Africa/Monrovia"
countries["LS"] = "Africa/Maseru" zones["LS"] = "Africa/Maseru"
countries["LT"] = "Europe/Vilnius" zones["LT"] = "Europe/Vilnius"
countries["LU"] = "Europe/Luxembourg" zones["LU"] = "Europe/Luxembourg"
countries["LV"] = "Europe/Riga" zones["LV"] = "Europe/Riga"
countries["LY"] = "Africa/Tripoli" zones["LY"] = "Africa/Tripoli"
countries["MA"] = "Africa/Casablanca" zones["MA"] = "Africa/Casablanca"
countries["MC"] = "Europe/Monaco" zones["MC"] = "Europe/Monaco"
countries["MD"] = "Europe/Chisinau" zones["MD"] = "Europe/Chisinau"
countries["MDT"] = "America/Denver" zones["MDT"] = "America/Denver"
countries["ME"] = "Europe/Podgorica" zones["ME"] = "Europe/Podgorica"
countries["MF"] = "America/Marigot" zones["MF"] = "America/Marigot"
countries["MG"] = "Indian/Antananarivo" zones["MG"] = "Indian/Antananarivo"
countries["MH"] = "Pacific/Majuro" zones["MH"] = "Pacific/Majuro"
countries["MK"] = "Europe/Skopje" zones["MK"] = "Europe/Skopje"
countries["ML"] = "Africa/Timbuktu" zones["ML"] = "Africa/Timbuktu"
countries["MM"] = "Asia/Yangon" zones["MM"] = "Asia/Yangon"
countries["MN"] = "Asia/Ulaanbaatar" zones["MN"] = "Asia/Ulaanbaatar"
countries["MO"] = "Asia/Macau" zones["MO"] = "Asia/Macau"
countries["MP"] = "Pacific/Saipan" zones["MP"] = "Pacific/Saipan"
countries["MQ"] = "America/Martinique" zones["MQ"] = "America/Martinique"
countries["MR"] = "Africa/Nouakchott" zones["MR"] = "Africa/Nouakchott"
countries["MS"] = "America/Montserrat" zones["MS"] = "America/Montserrat"
countries["MST"] = "America/Denver" zones["MST"] = "America/Denver"
countries["MT"] = "Europe/Malta" zones["MT"] = "Europe/Malta"
countries["MU"] = "Indian/Mauritius" zones["MU"] = "Indian/Mauritius"
countries["MV"] = "Indian/Maldives" zones["MV"] = "Indian/Maldives"
countries["MW"] = "Africa/Blantyre" zones["MW"] = "Africa/Blantyre"
countries["MX"] = "America/Mexico_City" zones["MX"] = "America/Mexico_City"
countries["MY"] = "Asia/Kuala_Lumpur" zones["MY"] = "Asia/Kuala_Lumpur"
countries["MZ"] = "Africa/Maputo" zones["MZ"] = "Africa/Maputo"
countries["NA"] = "Africa/Windhoek" zones["NA"] = "Africa/Windhoek"
countries["NC"] = "Pacific/Noumea" zones["NC"] = "Pacific/Noumea"
countries["NE"] = "Africa/Niamey" zones["NE"] = "Africa/Niamey"
countries["NF"] = "Pacific/Norfolk" zones["NF"] = "Pacific/Norfolk"
countries["NG"] = "Africa/Lagos" zones["NG"] = "Africa/Lagos"
countries["NI"] = "America/Managua" zones["NI"] = "America/Managua"
countries["NL"] = "Europe/Amsterdam" zones["NL"] = "Europe/Amsterdam"
countries["NO"] = "Europe/Oslo" zones["NO"] = "Europe/Oslo"
countries["NP"] = "Asia/Kathmandu" zones["NP"] = "Asia/Kathmandu"
countries["NR"] = "Pacific/Nauru" zones["NR"] = "Pacific/Nauru"
countries["NU"] = "Pacific/Niue" zones["NU"] = "Pacific/Niue"
countries["NZ"] = "Pacific/Auckland" zones["NZ"] = "Pacific/Auckland"
countries["OM"] = "Asia/Muscat" zones["OM"] = "Asia/Muscat"
countries["PA"] = "America/Panama" zones["PA"] = "America/Panama"
countries["PDT"] = "America/Los_Angeles" zones["PDT"] = "America/Los_Angeles"
countries["PE"] = "America/Lima" zones["PE"] = "America/Lima"
countries["PF"] = "Pacific/Tahiti" zones["PF"] = "Pacific/Tahiti"
countries["PG"] = "Pacific/Port_Moresby" zones["PG"] = "Pacific/Port_Moresby"
countries["PH"] = "Asia/Manila" zones["PH"] = "Asia/Manila"
countries["PK"] = "Asia/Karachi" zones["PK"] = "Asia/Karachi"
countries["PL"] = "Europe/Warsaw" zones["PL"] = "Europe/Warsaw"
countries["PM"] = "America/Miquelon" zones["PM"] = "America/Miquelon"
countries["PN"] = "Pacific/Pitcairn" zones["PN"] = "Pacific/Pitcairn"
countries["PR"] = "America/Puerto_Rico" zones["PR"] = "America/Puerto_Rico"
countries["PS"] = "Asia/Gaza" zones["PS"] = "Asia/Gaza"
countries["PST"] = "America/Los_Angeles" zones["PST"] = "America/Los_Angeles"
countries["PT"] = "Europe/Lisbon" zones["PT"] = "Europe/Lisbon"
countries["PW"] = "Pacific/Palau" zones["PW"] = "Pacific/Palau"
countries["PY"] = "America/Asuncion" zones["PY"] = "America/Asuncion"
countries["QA"] = "Asia/Qatar" zones["QA"] = "Asia/Qatar"
countries["RE"] = "Indian/Reunion" zones["RE"] = "Indian/Reunion"
countries["RO"] = "Europe/Bucharest" zones["RO"] = "Europe/Bucharest"
countries["RS"] = "Europe/Belgrade" zones["RS"] = "Europe/Belgrade"
countries["RU"] = "Europe/Moscow" zones["RU"] = "Europe/Moscow"
countries["RW"] = "Africa/Kigali" zones["RW"] = "Africa/Kigali"
countries["SA"] = "Asia/Riyadh" zones["SA"] = "Asia/Riyadh"
countries["SB"] = "Pacific/Guadalcanal" zones["SB"] = "Pacific/Guadalcanal"
countries["SC"] = "Indian/Mahe" zones["SC"] = "Indian/Mahe"
countries["SD"] = "Africa/Khartoum" zones["SD"] = "Africa/Khartoum"
countries["SE"] = "Europe/Stockholm" zones["SE"] = "Europe/Stockholm"
countries["SG"] = "Asia/Singapore" zones["SG"] = "Asia/Singapore"
countries["SH"] = "Atlantic/St_Helena" zones["SH"] = "Atlantic/St_Helena"
countries["SI"] = "Europe/Ljubljana" zones["SI"] = "Europe/Ljubljana"
countries["SJ"] = "Atlantic/Jan_Mayen" zones["SJ"] = "Atlantic/Jan_Mayen"
countries["SK"] = "Europe/Bratislava" zones["SK"] = "Europe/Bratislava"
countries["SL"] = "Africa/Freetown" zones["SL"] = "Africa/Freetown"
countries["SM"] = "Europe/San_Marino" zones["SM"] = "Europe/San_Marino"
countries["SN"] = "Africa/Dakar" zones["SN"] = "Africa/Dakar"
countries["SO"] = "Africa/Mogadishu" zones["SO"] = "Africa/Mogadishu"
countries["SR"] = "America/Paramaribo" zones["SR"] = "America/Paramaribo"
countries["SS"] = "Africa/Juba" zones["SS"] = "Africa/Juba"
countries["ST"] = "Africa/Sao_Tome" zones["ST"] = "Africa/Sao_Tome"
countries["SV"] = "America/El_Salvador" zones["SV"] = "America/El_Salvador"
countries["SX"] = "America/Lower_Princes" zones["SX"] = "America/Lower_Princes"
countries["SY"] = "Asia/Damascus" zones["SY"] = "Asia/Damascus"
countries["SZ"] = "Africa/Mbabane" zones["SZ"] = "Africa/Mbabane"
countries["TC"] = "America/Grand_Turk" zones["TC"] = "America/Grand_Turk"
countries["TD"] = "Africa/Ndjamena" zones["TD"] = "Africa/Ndjamena"
countries["TF"] = "Indian/Kerguelen" zones["TF"] = "Indian/Kerguelen"
countries["TG"] = "Africa/Lome" zones["TG"] = "Africa/Lome"
countries["TH"] = "Asia/Bangkok" zones["TH"] = "Asia/Bangkok"
countries["TJ"] = "Asia/Dushanbe" zones["TJ"] = "Asia/Dushanbe"
countries["TK"] = "Pacific/Fakaofo" zones["TK"] = "Pacific/Fakaofo"
countries["TL"] = "Asia/Dili" zones["TL"] = "Asia/Dili"
countries["TM"] = "Asia/Ashgabat" zones["TM"] = "Asia/Ashgabat"
countries["TN"] = "Africa/Tunis" zones["TN"] = "Africa/Tunis"
countries["TO"] = "Pacific/Tongatapu" zones["TO"] = "Pacific/Tongatapu"
countries["TR"] = "Europe/Istanbul" zones["TR"] = "Europe/Istanbul"
countries["TT"] = "America/Port_of_Spain" zones["TT"] = "America/Port_of_Spain"
countries["TV"] = "Pacific/Funafuti" zones["TV"] = "Pacific/Funafuti"
countries["TW"] = "Asia/Taipei" zones["TW"] = "Asia/Taipei"
countries["TZ"] = "Africa/Dar_es_Salaam" zones["TZ"] = "Africa/Dar_es_Salaam"
countries["UA"] = "Europe/Kiev" zones["UA"] = "Europe/Kiev"
countries["UG"] = "Africa/Kampala" zones["UG"] = "Africa/Kampala"
countries["UK"] = "Europe/London" zones["UK"] = "Europe/London"
countries["UM"] = "Pacific/Wake" zones["UM"] = "Pacific/Wake"
countries["US"] = "America/New_York" zones["US"] = "America/New_York"
countries["UTC"] = "UTC" zones["UTC"] = "UTC"
countries["UY"] = "America/Montevideo" zones["UY"] = "America/Montevideo"
countries["UZ"] = "Asia/Tashkent" zones["UZ"] = "Asia/Tashkent"
countries["VA"] = "Europe/Vatican" zones["VA"] = "Europe/Vatican"
countries["VC"] = "America/St_Vincent" zones["VC"] = "America/St_Vincent"
countries["VE"] = "America/Caracas" zones["VE"] = "America/Caracas"
countries["VG"] = "America/Tortola" zones["VG"] = "America/Tortola"
countries["VI"] = "America/St_Thomas" zones["VI"] = "America/St_Thomas"
countries["VN"] = "Asia/Ho_Chi_Minh" zones["VN"] = "Asia/Ho_Chi_Minh"
countries["VU"] = "Pacific/Efate" zones["VU"] = "Pacific/Efate"
countries["WF"] = "Pacific/Wallis" zones["WF"] = "Pacific/Wallis"
countries["WS"] = "Pacific/Apia" zones["WS"] = "Pacific/Apia"
countries["YE"] = "Asia/Aden" zones["YE"] = "Asia/Aden"
countries["YT"] = "Indian/Mayotte" zones["YT"] = "Indian/Mayotte"
countries["ZA"] = "Africa/Johannesburg" zones["ZA"] = "Africa/Johannesburg"
countries["ZM"] = "Africa/Lusaka" zones["ZM"] = "Africa/Lusaka"
countries["ZULU"] = "Zulu" zones["ZULU"] = "Zulu"
countries["ZW"] = "Africa/Harare" zones["ZW"] = "Africa/Harare"
@Suppress("MagicNumber")
ZoneId.getAvailableZoneIds().stream() ZoneId.getAvailableZoneIds().stream()
.filter { tz: String -> .filter { tz: String ->
tz.length <= 3 && !countries.containsKey(tz) tz.length <= 3 && !zones.containsKey(tz)
} }
.forEach { tz: String -> .forEach { tz: String ->
countries[tz] = tz zones[tz] = tz
} }
COUNTRIES_MAP = Collections.unmodifiableMap(countries) COUNTRIES_MAP = Collections.unmodifiableMap(zones)
} }
} }
override fun commandResponse( override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
sender: String, if (args.equals(ZONES_ARGS, true)) {
cmd: String, event.sendMessage("The supported countries/zones are: ")
args: String, event.sendList(COUNTRIES_MAP.keys.sorted().map { it.padEnd(4) }, 14, isIndent = true)
isPrivate: Boolean
) {
with(bot) {
if (args.isEmpty()) {
send(sender, "The supported countries/zones are: ", isPrivate)
@Suppress("MagicNumber")
sendList(
sender,
COUNTRIES_MAP.keys.sorted().map { it.padEnd(4) },
14,
isPrivate = false,
isIndent = true
)
} else { } else {
val msg = time(args) event.respond(time(args))
if (isPrivate) {
send(sender, msg.msg, true)
} else {
if (msg.isError) {
send(sender, msg.msg, false)
} else {
send(msg.msg)
}
}
}
} }
} }
@ -408,9 +387,9 @@ class WorldTime(bot: Mobibot) : AbstractModule(bot) {
init { init {
with(help) { with(help) {
add("To display a country's current date/time:") add("To display a country's current date/time:")
add(helpFormat("%c $TIME_CMD <country code>")) add(helpFormat("%c $TIME_CMD [<country code or zone>]"))
add("For a listing of the supported countries:") add("For a listing of the supported countries/zones:")
add(helpFormat("%c $TIME_CMD")) add(helpFormat("%c $TIME_CMD $ZONES_ARGS"))
} }
commands.add(TIME_CMD) commands.add(TIME_CMD)
} }

View file

@ -1,5 +1,5 @@
/* /*
* Sanitize.kt * ExceptionSanitizer.kt
* *
* Copyright (c) 2004-2021, Erik C. Thauvin (erik@thauvin.net) * Copyright (c) 2004-2021, Erik C. Thauvin (erik@thauvin.net)
* All rights reserved. * All rights reserved.
@ -36,27 +36,26 @@ import net.thauvin.erik.mobibot.Utils.obfuscate
import net.thauvin.erik.mobibot.Utils.replaceEach import net.thauvin.erik.mobibot.Utils.replaceEach
import net.thauvin.erik.mobibot.modules.ModuleException import net.thauvin.erik.mobibot.modules.ModuleException
object Sanitize { object ExceptionSanitizer {
/** /**
* Returns a sanitized exception to avoid displaying api keys, etc. in CI logs. * Returns a sanitized exception to avoid displaying api keys, etc. in CI logs.
*/ */
fun sanitizeException(e: ModuleException, vararg sanitize: String): ModuleException { fun ModuleException.sanitize(vararg sanitize: String): ModuleException {
var sanitizedException = e
val search = sanitize.filter { it.isNotBlank() }.toTypedArray() val search = sanitize.filter { it.isNotBlank() }.toTypedArray()
if (search.isNotEmpty()) { if (search.isNotEmpty()) {
val obfuscate = search.map { it.obfuscate() }.toTypedArray() val obfuscate = search.map { it.obfuscate() }.toTypedArray()
with(e) { with(this) {
if (cause?.message != null) { if (!cause?.message.isNullOrBlank()) {
sanitizedException = ModuleException( return ModuleException(
debugMessage, debugMessage,
cause!!.javaClass.name + ": " + cause!!.message!!.replaceEach(search, obfuscate), cause!!.javaClass.name + ": " + cause!!.message!!.replaceEach(search, obfuscate),
this this
) )
} else if (message != null) { } else if (!message.isNullOrBlank()) {
sanitizedException = ModuleException(debugMessage, message!!.replaceEach(search, obfuscate), this) return ModuleException(debugMessage, message!!.replaceEach(search, obfuscate), this)
} }
} }
} }
return sanitizedException return this
} }
} }

View file

@ -31,12 +31,14 @@
*/ */
package net.thauvin.erik.mobibot package net.thauvin.erik.mobibot
import assertk.assertThat
import assertk.assertions.contains
import assertk.assertions.isEqualTo
import assertk.assertions.isFailure
import assertk.assertions.isInstanceOf
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 org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.testng.annotations.Test import org.testng.annotations.Test
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.net.MalformedURLException import java.net.MalformedURLException
import java.net.UnknownHostException import java.net.UnknownHostException
@ -48,26 +50,24 @@ class FeedReaderTest {
@Test @Test
fun readFeedTest() { fun readFeedTest() {
var messages = readFeed("https://feeds.thauvin.net/ethauvin") var messages = readFeed("https://feeds.thauvin.net/ethauvin")
assertThat(messages.size).describedAs("messages = 10").isEqualTo(10) assertThat(messages.size, "size = 10").isEqualTo(10)
assertThat(messages[1].msg).describedAs("feed entry url").contains("ethauvin") assertThat(messages[1].msg, "feed entry url").contains("ethauvin")
messages = readFeed("https://lorem-rss.herokuapp.com/feed?length=0") messages = readFeed("https://lorem-rss.herokuapp.com/feed?length=0")
assertThat(messages[0].msg).describedAs("nothing to view").contains("nothing") assertThat(messages[0].msg, "nothing to view").contains("nothing")
messages = readFeed("https://lorem-rss.herokuapp.com/feed?length=42", 42) messages = readFeed("https://lorem-rss.herokuapp.com/feed?length=42", 42)
assertThat(messages.size).describedAs("messages = 84").isEqualTo(84) assertThat(messages.size, "messages = 84").isEqualTo(84)
assertThat(messages.last().msg).describedAs("example entry url").contains("http://example.com/test/") assertThat(messages.last().msg, "example entry url").contains("http://example.com/test/")
assertThatThrownBy { readFeed("blah") }.describedAs("invalid URL") assertThat { readFeed("blah") }.isFailure().isInstanceOf(MalformedURLException::class.java)
.isInstanceOf(MalformedURLException::class.java)
assertThatThrownBy { readFeed("https://www.example.com") }.describedAs("not a feed") assertThat { readFeed("https://www.example.com") }.isFailure().isInstanceOf(FeedException::class.java)
.isInstanceOf(FeedException::class.java)
assertThatThrownBy { readFeed("https://www.examples.com/foo") }.describedAs("404 not found") assertThat { readFeed("https://www.examples.com/foo") }.isFailure()
.isInstanceOf(FileNotFoundException::class.java) .isInstanceOf(FileNotFoundException::class.java)
assertThatThrownBy { readFeed("https://www.doesnotexists.com") }.describedAs("unknown host") assertThat { readFeed("https://www.doesnotexists.com") }.isFailure()
.isInstanceOf(UnknownHostException::class.java) .isInstanceOf(UnknownHostException::class.java)
} }
} }

View file

@ -1,5 +1,5 @@
/* /*
* PinboardUtilsTest.kt * PinboardTest.kt
* *
* Copyright (c) 2004-2021, Erik C. Thauvin (erik@thauvin.net) * Copyright (c) 2004-2021, Erik C. Thauvin (erik@thauvin.net)
* All rights reserved. * All rights reserved.
@ -32,45 +32,39 @@
package net.thauvin.erik.mobibot package net.thauvin.erik.mobibot
import net.thauvin.erik.mobibot.PinboardUtils.toTimestamp
import net.thauvin.erik.mobibot.entries.EntryLink import net.thauvin.erik.mobibot.entries.EntryLink
import net.thauvin.erik.pinboard.PinboardPoster
import org.testng.Assert.assertFalse import org.testng.Assert.assertFalse
import org.testng.Assert.assertTrue import org.testng.Assert.assertTrue
import org.testng.annotations.Test import org.testng.annotations.Test
import java.net.URL import java.net.URL
import java.util.Date
class PinboardUtilsTest : LocalProperties() { class PinboardTest : LocalProperties() {
private val pinboard = Pinboard()
@Test @Test
fun pinboardTest() { fun testPinboard() {
val apiToken = getProperty("pinboard-api-token") val apiToken = getProperty("pinboard-api-token")
val pinboard = PinboardPoster(apiToken)
val url = "https://www.example.com/" val url = "https://www.example.com/"
val ircServer = "irc.test.com" val ircServer = "irc.test.com"
val entry = EntryLink(url, "Test Example", "ErikT", "", "#mobitopia", listOf("test")) val entry = EntryLink(url, "Test Example", "ErikT", "", "#mobitopia", listOf("test"))
PinboardUtils.addPin(pinboard, ircServer, entry) pinboard.setApiToken(apiToken)
pinboard.addPin(ircServer, entry)
assertTrue(validatePin(apiToken, url = entry.link, entry.title, entry.nick, entry.channel), "validate add") assertTrue(validatePin(apiToken, url = entry.link, entry.title, entry.nick, entry.channel), "validate add")
entry.link = "https://www.foo.com/" entry.link = "https://www.foo.com/"
PinboardUtils.updatePin(pinboard, ircServer, url, entry) pinboard.updatePin(ircServer, url, entry)
assertTrue(validatePin(apiToken, url = entry.link, ircServer), "validate update") assertTrue(validatePin(apiToken, url = entry.link, ircServer), "validate update")
entry.title = "Foo Title" entry.title = "Foo Title"
PinboardUtils.updatePin(pinboard, ircServer, entry.link, entry) pinboard.updatePin(ircServer, entry.link, entry)
assertTrue(validatePin(apiToken, url = entry.link, entry.title), "validate title") assertTrue(validatePin(apiToken, url = entry.link, entry.title), "validate title")
PinboardUtils.deletePin(pinboard, entry) pinboard.deletePin(entry)
assertFalse(validatePin(apiToken, url = entry.link), "validate delete") assertFalse(validatePin(apiToken, url = entry.link), "validate delete")
} }
@Test
fun toTimestampTest() {
val d = Date()
assertTrue(d.toTimestamp().matches("[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z".toRegex()))
}
private fun validatePin(apiToken: String, url: String, vararg matches: String): Boolean { private fun validatePin(apiToken: String, url: String, vararg matches: String): Boolean {
val response = Utils.urlReader( val response = Utils.urlReader(
URL( URL(

View file

@ -31,6 +31,8 @@
*/ */
package net.thauvin.erik.mobibot package net.thauvin.erik.mobibot
import assertk.assertThat
import assertk.assertions.isEqualTo
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.buildCmdSyntax import net.thauvin.erik.mobibot.Utils.buildCmdSyntax
@ -55,8 +57,7 @@ import net.thauvin.erik.mobibot.Utils.unescapeXml
import net.thauvin.erik.mobibot.Utils.uptime import net.thauvin.erik.mobibot.Utils.uptime
import net.thauvin.erik.mobibot.Utils.urlReader import net.thauvin.erik.mobibot.Utils.urlReader
import net.thauvin.erik.mobibot.msg.Message.Companion.DEFAULT_COLOR import net.thauvin.erik.mobibot.msg.Message.Companion.DEFAULT_COLOR
import org.assertj.core.api.Assertions.assertThat import org.pircbotx.Colors
import org.jibble.pircbot.Colors
import org.testng.annotations.BeforeClass import org.testng.annotations.BeforeClass
import org.testng.annotations.Test import org.testng.annotations.Test
import java.io.File import java.io.File
@ -86,59 +87,59 @@ class UtilsTest {
val dir = "dir" val dir = "dir"
val sep = '/' val sep = '/'
val url = "https://erik.thauvin.net" val url = "https://erik.thauvin.net"
assertThat(dir.appendIfMissing(File.separatorChar)).describedAs("appendIfMissing(dir)") assertThat(dir.appendIfMissing(File.separatorChar), "appendIfMissing(dir)")
.isEqualTo(dir + File.separatorChar) .isEqualTo(dir + File.separatorChar)
assertThat(url.appendIfMissing(sep)).describedAs("appendIfMissing(url)").isEqualTo("$url$sep") assertThat(url.appendIfMissing(sep), "appendIfMissing(url)").isEqualTo("$url$sep")
assertThat("$url$sep".appendIfMissing(sep)).describedAs("appendIfMissing($url$sep)").isEqualTo("$url$sep") assertThat("$url$sep".appendIfMissing(sep), "appendIfMissing($url$sep)").isEqualTo("$url$sep")
} }
@Test @Test
fun testBold() { fun testBold() {
assertThat(bold(1)).describedAs("bold(1)").isEqualTo(Colors.BOLD + "1" + Colors.BOLD) assertThat(bold(1), "bold(1)").isEqualTo(Colors.BOLD + "1" + Colors.BOLD)
assertThat(bold(2L)).describedAs("bold(1)").isEqualTo(Colors.BOLD + "2" + Colors.BOLD) assertThat(bold(2L), "bold(1)").isEqualTo(Colors.BOLD + "2" + Colors.BOLD)
assertThat(bold(ascii)).describedAs("bold(ascii)").isEqualTo(Colors.BOLD + ascii + Colors.BOLD) assertThat(bold(ascii), "bold(ascii)").isEqualTo(Colors.BOLD + ascii + Colors.BOLD)
assertThat(bold("test")).describedAs("bold(test)").isEqualTo(Colors.BOLD + "test" + Colors.BOLD) assertThat(bold("test"), "bold(test)").isEqualTo(Colors.BOLD + "test" + Colors.BOLD)
} }
@Test @Test
fun testBuildCmdSyntax() { fun testBuildCmdSyntax() {
val bot = "mobibot" val bot = "mobibot"
assertThat(buildCmdSyntax("%c $test %n $test", bot, false)).describedAs("public") assertThat(buildCmdSyntax("%c $test %n $test", bot, false), "public")
.isEqualTo("$bot: $test $bot $test") .isEqualTo("$bot: $test $bot $test")
assertThat(buildCmdSyntax("%c %n $test %c $test %n", bot, true)).describedAs("public") assertThat(buildCmdSyntax("%c %n $test %c $test %n", bot, true), "public")
.isEqualTo("/msg $bot $bot $test /msg $bot $test $bot") .isEqualTo("/msg $bot $bot $test /msg $bot $test $bot")
} }
@Test @Test
fun testCapitalise() { fun testCapitalise() {
assertThat("test".capitalise()).describedAs("capitalize(test)").isEqualTo("Test") assertThat("test".capitalise(), "capitalize(test)").isEqualTo("Test")
assertThat("Test".capitalise()).describedAs("capitalize(Test)").isEqualTo("Test") assertThat("Test".capitalise(), "capitalize(Test)").isEqualTo("Test")
assertThat(test.capitalise()).describedAs("capitalize($test)").isEqualTo(test) assertThat(test.capitalise(), "capitalize($test)").isEqualTo(test)
assertThat("".capitalise()).describedAs("capitalize()").isEqualTo("") assertThat("".capitalise(), "capitalize()").isEqualTo("")
} }
@Test @Test
fun textCapitaliseWords() { fun textCapitaliseWords() {
assertThat(test.capitalizeWords()).describedAs("captiatlizeWords(test)").isEqualTo("This Is A Test.") assertThat(test.capitalizeWords(), "captiatlizeWords(test)").isEqualTo("This Is A Test.")
assertThat("Already Capitalized".capitalizeWords()).describedAs("already capitalized") assertThat("Already Capitalized".capitalizeWords(), "already capitalized")
.isEqualTo("Already Capitalized") .isEqualTo("Already Capitalized")
assertThat(" a test ".capitalizeWords()).describedAs("with spaces").isEqualTo(" A Test ") assertThat(" a test ".capitalizeWords(), "with spaces").isEqualTo(" A Test ")
} }
@Test @Test
fun testColorize() { fun testColorize() {
assertThat(colorize(ascii, Colors.REVERSE)).describedAs("colorize(reverse)").isEqualTo( assertThat(colorize(ascii, Colors.REVERSE), "colorize(reverse)").isEqualTo(
Colors.REVERSE + ascii + Colors.REVERSE Colors.REVERSE + ascii + Colors.REVERSE
) )
assertThat(colorize(ascii, Colors.RED)).describedAs("colorize(red)") assertThat(colorize(ascii, Colors.RED), "colorize(red)")
.isEqualTo(Colors.RED + ascii + Colors.NORMAL) .isEqualTo(Colors.RED + ascii + Colors.NORMAL)
assertThat(colorize(ascii, Colors.BOLD)).describedAs("colorized(bold)") assertThat(colorize(ascii, Colors.BOLD), "colorized(bold)")
.isEqualTo(Colors.BOLD + ascii + Colors.BOLD) .isEqualTo(Colors.BOLD + ascii + Colors.BOLD)
assertThat(colorize(null, Colors.RED)).describedAs("colorize(null)").isEqualTo("") assertThat(colorize(null, Colors.RED), "colorize(null)").isEqualTo("")
assertThat(colorize("", Colors.RED)).describedAs("colorize()").isEqualTo("") assertThat(colorize("", Colors.RED), "colorize()").isEqualTo("")
assertThat(colorize(ascii, DEFAULT_COLOR)).describedAs("colorize(none)").isEqualTo(ascii) assertThat(colorize(ascii, DEFAULT_COLOR), "colorize(none)").isEqualTo(ascii)
assertThat(colorize(" ", Colors.NORMAL)).describedAs("colorize(blank)") assertThat(colorize(" ", Colors.NORMAL), "colorize(blank)")
.isEqualTo(Colors.NORMAL + " " + Colors.NORMAL) .isEqualTo(Colors.NORMAL + " " + Colors.NORMAL)
} }
@ -157,9 +158,9 @@ class UtilsTest {
val p = Properties() val p = Properties()
p["one"] = "1" p["one"] = "1"
p["two"] = "two" p["two"] = "two"
assertThat(p.getIntProperty("one", 9)).describedAs("getIntProperty(one)").isEqualTo(1) assertThat(p.getIntProperty("one", 9), "getIntProperty(one)").isEqualTo(1)
assertThat(p.getIntProperty("two", 2)).describedAs("getIntProperty(two)").isEqualTo(2) assertThat(p.getIntProperty("two", 2), "getIntProperty(two)").isEqualTo(2)
assertThat(p.getIntProperty("foo", 3)).describedAs("getIntProperty(foo)").isEqualTo(3) assertThat(p.getIntProperty("foo", 3), "getIntProperty(foo)").isEqualTo(3)
} }
@Test @Test
@ -169,26 +170,26 @@ class UtilsTest {
@Test @Test
fun testHelpFormat() { fun testHelpFormat() {
assertThat(helpFormat(test, isBold = true, isIndent = false)).describedAs("bold") assertThat(helpFormat(test, isBold = true, isIndent = false), "bold")
.isEqualTo("${Colors.BOLD}$test${Colors.BOLD}") .isEqualTo("${Colors.BOLD}$test${Colors.BOLD}")
assertThat(helpFormat(test, isBold = false, isIndent = true)).describedAs("indent") assertThat(helpFormat(test, isBold = false, isIndent = true), "indent")
.isEqualTo(test.prependIndent()) .isEqualTo(test.prependIndent())
assertThat(helpFormat(test, isBold = true, isIndent = true)).describedAs("bold-indent") assertThat(helpFormat(test, isBold = true, isIndent = true), "bold-indent")
.isEqualTo(colorize(test, Colors.BOLD).prependIndent()) .isEqualTo(colorize(test, Colors.BOLD).prependIndent())
} }
@Test @Test
fun testIsoLocalDate() { fun testIsoLocalDate() {
assertThat(cal.time.toIsoLocalDate()).describedAs("isoLocalDate(date)").isEqualTo("1952-02-17") assertThat(cal.time.toIsoLocalDate(), "isoLocalDate(date)").isEqualTo("1952-02-17")
assertThat(localDateTime.toIsoLocalDate()).describedAs("isoLocalDate(localDate)").isEqualTo("1952-02-17") assertThat(localDateTime.toIsoLocalDate(), "isoLocalDate(localDate)").isEqualTo("1952-02-17")
} }
@Test @Test
fun testObfuscate() { fun testObfuscate() {
assertThat(ascii.obfuscate().length).describedAs("obfuscate is right length").isEqualTo(ascii.length) assertThat(ascii.obfuscate().length, "obfuscate is right length").isEqualTo(ascii.length)
assertThat(ascii.obfuscate()).describedAs("obfuscate()").isEqualTo("x".repeat(ascii.length)) assertThat(ascii.obfuscate(), "obfuscate()").isEqualTo("x".repeat(ascii.length))
assertThat(" ".obfuscate()).describedAs("obfuscate(blank)").isEqualTo(" ") assertThat(" ".obfuscate(), "obfuscate(blank)").isEqualTo(" ")
} }
@Test @Test
@ -197,7 +198,7 @@ class UtilsTest {
val weeks = "weeks" val weeks = "weeks"
for (i in -1..3) { for (i in -1..3) {
assertThat(week.plural(i.toLong())).describedAs("plural($i)").isEqualTo(if (i > 1) weeks else week) assertThat(week.plural(i.toLong()), "plural($i)").isEqualTo(if (i > 1) weeks else week)
} }
} }
@ -205,15 +206,15 @@ class UtilsTest {
fun testReplaceEach() { fun testReplaceEach() {
val search = arrayOf("one", "two", "three") val search = arrayOf("one", "two", "three")
val replace = arrayOf("1", "2", "3") val replace = arrayOf("1", "2", "3")
assertThat(search.joinToString(",").replaceEach(search, replace)).describedAs("replaceEach(1,2,3") assertThat(search.joinToString(",").replaceEach(search, replace), "replaceEach(1,2,3")
.isEqualTo(replace.joinToString(",")) .isEqualTo(replace.joinToString(","))
assertThat(test.replaceEach(search, replace)).describedAs("replaceEach(nothing)").isEqualTo(test) assertThat(test.replaceEach(search, replace), "replaceEach(nothing)").isEqualTo(test)
assertThat(test.replaceEach(arrayOf("t", "e"), arrayOf("", "E"))).describedAs("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"))
assertThat(test.replaceEach(search, emptyArray())).describedAs("replaceEach(search, empty)") assertThat(test.replaceEach(search, emptyArray()), "replaceEach(search, empty)")
.isEqualTo(test) .isEqualTo(test)
} }
@ -234,8 +235,8 @@ class UtilsTest {
@Test @Test
fun testToIntOrDefault() { fun testToIntOrDefault() {
assertThat("10".toIntOrDefault(1)).describedAs("toIntOrDefault(10, 1)").isEqualTo(10) assertThat("10".toIntOrDefault(1), "toIntOrDefault(10, 1)").isEqualTo(10)
assertThat("a".toIntOrDefault(2)).describedAs("toIntOrDefault(a, 2)").isEqualTo(2) assertThat("a".toIntOrDefault(2), "toIntOrDefault(a, 2)").isEqualTo(2)
} }
@Test @Test
@ -247,26 +248,26 @@ class UtilsTest {
@Test @Test
fun testUptime() { fun testUptime() {
assertThat(uptime(547800300076L)).describedAs("full") assertThat(uptime(547800300076L), "full")
.isEqualTo("17 years 2 months 2 weeks 1 day 6 hours 45 minutes") .isEqualTo("17 years 2 months 2 weeks 1 day 6 hours 45 minutes")
assertThat(uptime(2700000L)).describedAs("minutes").isEqualTo("45 minutes") assertThat(uptime(2700000L), "minutes").isEqualTo("45 minutes")
assertThat(uptime(24300000L)).describedAs("hours minutes").isEqualTo("6 hours 45 minutes") assertThat(uptime(24300000L), "hours minutes").isEqualTo("6 hours 45 minutes")
assertThat(uptime(110700000L)).describedAs("days hours minutes").isEqualTo("1 day 6 hours 45 minutes") assertThat(uptime(110700000L), "days hours minutes").isEqualTo("1 day 6 hours 45 minutes")
assertThat(uptime(1320300000L)).describedAs("weeks days hours minutes") assertThat(uptime(1320300000L), "weeks days hours minutes")
.isEqualTo("2 weeks 1 day 6 hours 45 minutes") .isEqualTo("2 weeks 1 day 6 hours 45 minutes")
assertThat(uptime(0L)).describedAs("0 minutes").isEqualTo("0 minute") assertThat(uptime(0L), "0 minutes").isEqualTo("0 minute")
} }
@Test @Test
@Throws(IOException::class) @Throws(IOException::class)
fun testUrlReader() { fun testUrlReader() {
assertThat(urlReader(URL("https://postman-echo.com/status/200"))).describedAs("urlReader()") assertThat(urlReader(URL("https://postman-echo.com/status/200")), "urlReader()")
.isEqualTo("{\"status\":200}") .isEqualTo("{\"status\":200}")
} }
@Test @Test
fun testUtcDateTime() { fun testUtcDateTime() {
assertThat(cal.time.toUtcDateTime()).describedAs("utcDateTime(date)").isEqualTo("1952-02-17 12:30") assertThat(cal.time.toUtcDateTime(), "utcDateTime(date)").isEqualTo("1952-02-17 12:30")
assertThat(localDateTime.toUtcDateTime()).describedAs("utcDateTime(localDate)").isEqualTo("1952-02-17 12:30") assertThat(localDateTime.toUtcDateTime(), "utcDateTime(localDate)").isEqualTo("1952-02-17 12:30")
} }
} }

View file

@ -31,7 +31,12 @@
*/ */
package net.thauvin.erik.mobibot.commands.tell package net.thauvin.erik.mobibot.commands.tell
import org.assertj.core.api.Assertions.assertThat import assertk.all
import assertk.assertThat
import assertk.assertions.isEqualTo
import assertk.assertions.isFalse
import assertk.assertions.isTrue
import assertk.assertions.prop
import org.testng.annotations.Test import org.testng.annotations.Test
import java.time.Duration import java.time.Duration
import java.time.LocalDateTime import java.time.LocalDateTime
@ -51,18 +56,21 @@ class TellMessageTest {
val recipient = "recipient" val recipient = "recipient"
val sender = "sender" val sender = "sender"
val tellMessage = TellMessage(sender, recipient, message) val tellMessage = TellMessage(sender, recipient, message)
assertThat(tellMessage).extracting("sender", "recipient", "message") assertThat(tellMessage).all {
.containsExactly(sender, recipient, message) prop(TellMessage::sender).isEqualTo(sender)
assertThat(isValidDate(tellMessage.queued)).describedAs("queued is valid date/time").isTrue prop(TellMessage::recipient).isEqualTo(recipient)
assertThat(tellMessage.isMatch(sender)).describedAs("match sender").isTrue prop(TellMessage::message).isEqualTo(message)
assertThat(tellMessage.isMatch(recipient)).describedAs("match recipient").isTrue }
assertThat(tellMessage.isMatch("foo")).describedAs("foo is no match").isFalse assertThat(isValidDate(tellMessage.queued), "queued is valid date/time").isTrue()
assertThat(tellMessage.isMatch(sender), "match sender").isTrue()
assertThat(tellMessage.isMatch(recipient), "match recipient").isTrue()
assertThat(tellMessage.isMatch("foo"), "foo is no match").isFalse()
tellMessage.isReceived = false tellMessage.isReceived = false
assertThat(tellMessage.receptionDate).describedAs("reception date not set").isEqualTo(LocalDateTime.MIN) assertThat(tellMessage.receptionDate, "reception date not set").isEqualTo(LocalDateTime.MIN)
tellMessage.isReceived = true tellMessage.isReceived = true
assertThat(tellMessage.isReceived).describedAs("is received").isTrue assertThat(tellMessage.isReceived, "is received").isTrue()
assertThat(isValidDate(tellMessage.receptionDate)).describedAs("received is valid date/time").isTrue assertThat(isValidDate(tellMessage.receptionDate), "received is valid date/time").isTrue()
tellMessage.isNotified = true tellMessage.isNotified = true
assertThat(tellMessage.isNotified).describedAs("is notified").isTrue assertThat(tellMessage.isNotified, "is notified").isTrue()
} }
} }

View file

@ -31,9 +31,16 @@
*/ */
package net.thauvin.erik.mobibot.entries package net.thauvin.erik.mobibot.entries
import assertk.all
import assertk.assertThat
import assertk.assertions.isEmpty
import assertk.assertions.isEqualTo
import assertk.assertions.isFalse
import assertk.assertions.isTrue
import assertk.assertions.prop
import assertk.assertions.size
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.assertj.core.api.Assertions.assertThat
import org.testng.annotations.Test import org.testng.annotations.Test
import java.security.SecureRandom import java.security.SecureRandom
import java.util.Date import java.util.Date
@ -58,21 +65,30 @@ class EntryLinkTest {
entryLink.addComment("c$i", "u$i") entryLink.addComment("c$i", "u$i")
i++ i++
} }
assertThat(entryLink.comments.size).describedAs("getComments().size() == 5").isEqualTo(i) assertThat(entryLink.comments.size, "getComments().size() == 5").isEqualTo(i)
i = 0 i = 0
for (comment in entryLink.comments) { for (comment in entryLink.comments) {
assertThat(comment).extracting("comment", "nick").containsExactly("c$i", "u$i") assertThat(comment).all {
prop(EntryComment::comment).isEqualTo("c$i")
prop(EntryComment::nick).isEqualTo("u$i")
}
i++ i++
} }
val r = SecureRandom() val r = SecureRandom()
while (entryLink.comments.size > 0) { while (entryLink.comments.size > 0) {
entryLink.deleteComment(r.nextInt(entryLink.comments.size)) entryLink.deleteComment(r.nextInt(entryLink.comments.size))
} }
assertThat(entryLink.comments).describedAs("hasComments()").isEmpty() assertThat(entryLink.comments, "hasComments()").isEmpty()
entryLink.addComment("nothing", "nobody") entryLink.addComment("nothing", "nobody")
entryLink.setComment(0, "something", "somebody") entryLink.setComment(0, "something", "somebody")
assertThat(entryLink.getComment(0)).describedAs("get first comment").extracting("nick", "comment") val comment = entryLink.getComment(0)
.containsExactly("somebody", "something") assertThat(comment, "get first comment").all {
prop(EntryComment::nick).isEqualTo("somebody")
prop(EntryComment::comment).isEqualTo("something")
}
assertThat(entryLink.deleteComment(comment), "delete comment").isTrue()
assertThat(entryLink.deleteComment(comment), "comment is already deleted").isFalse()
} }
@Test @Test
@ -80,19 +96,19 @@ class EntryLinkTest {
val tag = "test" val tag = "test"
val tags = listOf(SyndCategoryImpl().apply { name = tag }) val tags = listOf(SyndCategoryImpl().apply { name = tag })
val link = EntryLink("link", "title", "nick", "channel", Date(), tags) val link = EntryLink("link", "title", "nick", "channel", Date(), tags)
assertThat(link.tags.size).describedAs("check tag size").isEqualTo(tags.size) assertThat(link.tags.size, "check tag size").isEqualTo(tags.size)
assertThat(link.tags[0].name).describedAs("check tag name").isEqualTo(tag) assertThat(link.tags[0].name, "check tag name").isEqualTo(tag)
assertThat(link.pinboardTags).describedAs("check pinboard tags").isEqualTo("nick,$tag") assertThat(link.pinboardTags, "check pinboard tags").isEqualTo("nick,$tag")
} }
@Test @Test
fun testMatches() { fun testMatches() {
assertThat(entryLink.matches("mobitopia")).describedAs("match mobitopia").isTrue assertThat(entryLink.matches("mobitopia"), "match mobitopia").isTrue()
assertThat(entryLink.matches("skynx")).describedAs("match nick").isTrue assertThat(entryLink.matches("skynx"), "match nick").isTrue()
assertThat(entryLink.matches("www.mobitopia.org")).describedAs("match url").isTrue assertThat(entryLink.matches("www.mobitopia.org"), "match url").isTrue()
assertThat(entryLink.matches("foo")).describedAs("match foo").isFalse assertThat(entryLink.matches("foo"), "match foo").isFalse()
assertThat(entryLink.matches("")).describedAs("match empty").isFalse assertThat(entryLink.matches("<empty>"), "match empty").isFalse()
assertThat(entryLink.matches(null)).describedAs("match null").isFalse assertThat(entryLink.matches(null), "match null").isFalse()
} }
@ -100,20 +116,19 @@ class EntryLinkTest {
fun testTags() { fun testTags() {
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).describedAs("tag.getName($i)").isEqualTo("tag" + (i + 1)) assertThat(tag.name, "tag.getName($i)").isEqualTo("tag" + (i + 1))
} }
assertThat(entryLink.tags.size).describedAs("getTags().size() is 5").isEqualTo(5) assertThat(entryLink.tags, "size is 5").size().isEqualTo(5)
assertThat(entryLink.tags).describedAs("hasTags() is true").isNotEmpty
entryLink.setTags("-tag5") entryLink.setTags("-tag5")
entryLink.setTags("+mobitopia") entryLink.setTags("+mobitopia")
entryLink.setTags("tag4") entryLink.setTags("tag4")
entryLink.setTags("-mobitopia") entryLink.setTags("-mobitopia")
assertThat(entryLink.pinboardTags).describedAs("getPinboardTags()") assertThat(entryLink.pinboardTags, "getPinboardTags()")
.isEqualTo(entryLink.nick + ",tag1,tag2,tag3,tag4,mobitopia") .isEqualTo(entryLink.nick + ",tag1,tag2,tag3,tag4,mobitopia")
val size = entryLink.tags.size val size = entryLink.tags.size
entryLink.setTags("") entryLink.setTags("")
assertThat(entryLink.tags.size).describedAs("empty tag").isEqualTo(size) assertThat(entryLink.tags.size, "empty tag").isEqualTo(size)
entryLink.setTags(" ") entryLink.setTags(" ")
assertThat(entryLink.tags.size).describedAs("blank tag").isEqualTo(size) assertThat(entryLink.tags.size, "blank tag").isEqualTo(size)
} }
} }

View file

@ -0,0 +1,121 @@
/*
* FeedMgrTest.kt
*
* Copyright (c) 2004-2021, Erik C. Thauvin (erik@thauvin.net)
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* Neither the name of this project nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.thauvin.erik.mobibot.entries
import assertk.all
import assertk.assertThat
import assertk.assertions.endsWith
import assertk.assertions.isEqualTo
import assertk.assertions.isTrue
import assertk.assertions.prop
import net.thauvin.erik.mobibot.Utils.today
import org.testng.annotations.BeforeSuite
import org.testng.annotations.Test
import java.nio.file.Paths
import java.util.Date
import kotlin.io.path.absolutePathString
import kotlin.io.path.deleteIfExists
import kotlin.io.path.exists
import kotlin.io.path.fileSize
import kotlin.io.path.name
class FeedMgrTest {
private val entries = Entries()
private val channel = "mobibot"
@BeforeSuite(alwaysRun = true)
fun beforeSuite() {
entries.logsDir = "src/test/resources/"
entries.ircServer = "irc.example.com"
entries.channel = channel
entries.backlogs = "https://www.mobitopia.org/mobibot/logs"
}
@Test
fun testFeedMgr() {
// Load the feed
assertThat(FeedsMgr.loadFeed(entries), "pubDate").isEqualTo("2021-10-31")
assertThat(entries.links.size, "2 links").isEqualTo(2)
entries.links.forEachIndexed { i, entryLink ->
assertThat(entryLink, "Example $(i + 1)").all {
prop(EntryLink::title).isEqualTo("Example ${i + 1}")
prop(EntryLink::link).isEqualTo("https://www.example.com/${i + 1}")
prop(EntryLink::channel).isEqualTo(channel)
}
entryLink.tags.forEachIndexed { y, tag ->
assertThat(tag.name, "tag${i + 1}-${y + 1}").isEqualTo("tag${i + 1}-${y + 1}")
}
}
with(entries.links.first()) {
assertThat(nick, "first nick").isEqualTo("ErikT")
assertThat(date, "first date").isEqualTo(Date(1635638400000L))
comments.forEachIndexed { i, entryComment ->
assertThat(entryComment.comment, "comment ${i + 1}").endsWith("comment ${i + 1}.")
if (i == 0) {
assertThat(entryComment.nick, "comment ${i + 1} nick").isEqualTo("ErikT")
} else {
assertThat(entryComment.nick, "comment ${i + 1} nick").isEqualTo("Skynx")
}
}
}
assertThat(entries.links[1], "second link").all {
prop(EntryLink::nick).isEqualTo("Skynx")
prop(EntryLink::date).isEqualTo(Date(1635638460000L))
}
val currentFile = Paths.get("${entries.logsDir}test.xml")
val backlogFile = Paths.get("${entries.logsDir}${today()}.xml")
// Save the feed
FeedsMgr.saveFeed(entries, currentFile.name)
assertThat(currentFile.exists(), "${currentFile.absolutePathString()} exists").isTrue()
assertThat(backlogFile.exists(), "${backlogFile.absolutePathString()} exits").isTrue()
assertThat(currentFile.fileSize(), "files are identical").isEqualTo(backlogFile.fileSize())
// Load the test feed
entries.links.clear()
FeedsMgr.loadFeed(entries, currentFile.name)
entries.links.forEachIndexed { i, entryLink ->
assertThat(entryLink.title, "${currentFile.name} title ${i + 1}").isEqualTo("Example ${i + 1}")
}
assertThat(currentFile.deleteIfExists(), "delete ${currentFile.absolutePathString()}").isTrue()
assertThat(backlogFile.deleteIfExists(), "delete ${backlogFile.absolutePathString()}").isTrue()
}
}

View file

@ -31,11 +31,13 @@
*/ */
package net.thauvin.erik.mobibot.modules package net.thauvin.erik.mobibot.modules
import assertk.assertThat
import assertk.assertions.isEqualTo
import assertk.assertions.isFailure
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.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.testng.annotations.Test import org.testng.annotations.Test
/** /**
@ -44,11 +46,11 @@ import org.testng.annotations.Test
class CalcTest { class CalcTest {
@Test @Test
fun testCalculate() { fun testCalculate() {
assertThat(calculate("1 + 1")).describedAs("calculate(1+1)").isEqualTo("1+1 = %s", bold(2)) assertThat(calculate("1 + 1"), "calculate(1+1)").isEqualTo("1+1 = ${bold(2)}")
assertThat(calculate("1 -3")).describedAs("calculate(1 -3)").isEqualTo("1-3 = %s", bold(-2)) assertThat(calculate("1 -3"), "calculate(1 -3)").isEqualTo("1-3 = ${bold(-2)}")
assertThat(calculate("pi+π+e+φ")).describedAs("calculate(pi+π+e+φ)") assertThat(calculate("pi+π+e+φ"), "calculate(pi+π+e+φ)")
.isEqualTo("pi+π+e+φ = %s", bold("10.62")) .isEqualTo("pi+π+e+φ = ${bold("10.62")}")
assertThatThrownBy { calculate("one + one") }.describedAs("calculate(one+one)") assertThat { calculate("one + one") }
.isInstanceOf(UnknownFunctionOrVariableException::class.java) .isFailure().isInstanceOf(UnknownFunctionOrVariableException::class.java)
} }
} }

View file

@ -31,8 +31,13 @@
*/ */
package net.thauvin.erik.mobibot.modules package net.thauvin.erik.mobibot.modules
import assertk.all
import assertk.assertThat
import assertk.assertions.isEqualTo
import assertk.assertions.isGreaterThan
import assertk.assertions.prop
import net.thauvin.erik.crypto.CryptoPrice
import net.thauvin.erik.mobibot.modules.CryptoPrices.Companion.currentPrice import net.thauvin.erik.mobibot.modules.CryptoPrices.Companion.currentPrice
import org.assertj.core.api.Assertions.assertThat
import org.testng.annotations.Test import org.testng.annotations.Test
/** /**
@ -43,11 +48,17 @@ class CryptoPricesTest {
@Throws(ModuleException::class) @Throws(ModuleException::class)
fun testMarketPrice() { fun testMarketPrice() {
var price = currentPrice(listOf("BTC")) var price = currentPrice(listOf("BTC"))
assertThat(price).extracting("base", "currency").containsExactly("BTC", "USD") assertThat(price, "BTC in USD").all {
assertThat(price.amount.signum()).describedAs("BTC > 0").isGreaterThan(0) prop(CryptoPrice::base).isEqualTo("BTC")
prop(CryptoPrice::currency).isEqualTo("USD")
prop(CryptoPrice::amount).transform { it.signum() }.isGreaterThan(0)
}
price = currentPrice(listOf("ETH", "EUR")) price = currentPrice(listOf("ETH", "EUR"))
assertThat(price).extracting("base", "currency").containsExactly("ETH", "EUR") assertThat(price, "ETH in EUR").all {
assertThat(price.amount.signum()).describedAs("ETH > 0").isGreaterThan(0) prop(CryptoPrice::base).isEqualTo("ETH")
prop(CryptoPrice::currency).isEqualTo("EUR")
prop(CryptoPrice::amount).transform { it.signum() }.isGreaterThan(0)
}
} }
} }

View file

@ -31,10 +31,21 @@
*/ */
package net.thauvin.erik.mobibot.modules package net.thauvin.erik.mobibot.modules
import assertk.all
import assertk.assertThat
import assertk.assertions.any
import assertk.assertions.contains
import assertk.assertions.isEqualTo
import assertk.assertions.isInstanceOf
import assertk.assertions.matches
import assertk.assertions.prop
import assertk.assertions.size
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.currencyRates import net.thauvin.erik.mobibot.modules.CurrencyConverter.Companion.currencyRates
import net.thauvin.erik.mobibot.modules.CurrencyConverter.Companion.loadRates import net.thauvin.erik.mobibot.modules.CurrencyConverter.Companion.loadRates
import org.assertj.core.api.Assertions.assertThat import net.thauvin.erik.mobibot.msg.ErrorMessage
import net.thauvin.erik.mobibot.msg.Message
import net.thauvin.erik.mobibot.msg.PublicMessage
import org.testng.annotations.BeforeClass import org.testng.annotations.BeforeClass
import org.testng.annotations.Test import org.testng.annotations.Test
@ -50,14 +61,28 @@ class CurrencyConverterTest {
@Test @Test
fun testConvertCurrency() { fun testConvertCurrency() {
assertThat(convertCurrency("100 USD to EUR").msg) assertThat(
.describedAs("100 USD to EUR").matches("\\$100\\.00 = €\\d{2,3}\\.\\d{2}") convertCurrency("100 USD to EUR").msg,
assertThat(convertCurrency("100 USD to USD").msg).describedAs("100 USD to USD") "100 USD to EUR"
.contains("You're kidding, right?") ).matches("\\$100\\.00 = €\\d{2,3}\\.\\d{2}".toRegex())
assertThat(convertCurrency("100 USD").msg).describedAs("100 USD").contains("Invalid query.") assertThat(convertCurrency("100 USD to USD"), "100 USD to USD").all {
prop(Message::msg).contains("You're kidding, right?")
isInstanceOf(PublicMessage::class.java)
}
assertThat(convertCurrency("100 USD"), "100 USD").all {
prop(Message::msg).contains("Invalid query.")
isInstanceOf(ErrorMessage::class.java)
}
}
@Test
fun testCurrencyRates() {
val rates = currencyRates() val rates = currencyRates()
assertThat(rates.size).describedAs("currencyRates.size == 33").isEqualTo(33) assertThat(rates).all {
assertThat(rates).describedAs("currencyRates(EUR< USD)").contains("EUR: 1") size().isEqualTo(33)
.anyMatch { it.matches("USD: .*".toRegex()) } any { it.matches("[A-Z]{3}: +[\\d.]+".toRegex()) }
contains("EUR: 1")
}
} }
} }

View file

@ -33,14 +33,15 @@
package net.thauvin.erik.mobibot.modules package net.thauvin.erik.mobibot.modules
import org.assertj.core.api.Assertions.assertThat import assertk.assertThat
import assertk.assertions.isEqualTo
import org.testng.annotations.Test import org.testng.annotations.Test
class DiceTest { class DiceTest {
@Test @Test
fun testWinLoseOrTie() { fun testWinLoseOrTie() {
assertThat(Dice.winLoseOrTie(6, 6)).describedAs("6 vs. 6").isEqualTo(Dice.Result.TIE) assertThat(Dice.winLoseOrTie(6, 6), "6 vs. 6").isEqualTo(Dice.Result.TIE)
assertThat(Dice.winLoseOrTie(6, 5)).describedAs("6 vs. 5").isEqualTo(Dice.Result.WIN) assertThat(Dice.winLoseOrTie(6, 5), "6 vs. 5").isEqualTo(Dice.Result.WIN)
assertThat(Dice.winLoseOrTie(5, 6)).describedAs("5 vs. 6").isEqualTo(Dice.Result.LOSE) assertThat(Dice.winLoseOrTie(5, 6), "5 vs. 6").isEqualTo(Dice.Result.LOSE)
} }
} }

View file

@ -31,11 +31,20 @@
*/ */
package net.thauvin.erik.mobibot.modules package net.thauvin.erik.mobibot.modules
import assertk.all
import assertk.assertThat
import assertk.assertions.contains
import assertk.assertions.hasNoCause
import assertk.assertions.isEqualTo
import assertk.assertions.isFailure
import assertk.assertions.isInstanceOf
import assertk.assertions.isNotEmpty
import assertk.assertions.prop
import net.thauvin.erik.mobibot.ExceptionSanitizer.sanitize
import net.thauvin.erik.mobibot.LocalProperties import net.thauvin.erik.mobibot.LocalProperties
import net.thauvin.erik.mobibot.Sanitize.sanitizeException
import net.thauvin.erik.mobibot.modules.GoogleSearch.Companion.searchGoogle import net.thauvin.erik.mobibot.modules.GoogleSearch.Companion.searchGoogle
import org.assertj.core.api.Assertions.assertThat import net.thauvin.erik.mobibot.msg.ErrorMessage
import org.assertj.core.api.Assertions.assertThatThrownBy import net.thauvin.erik.mobibot.msg.Message
import org.testng.annotations.Test import org.testng.annotations.Test
/** /**
@ -49,23 +58,33 @@ class GoogleSearchTest : LocalProperties() {
val cseKey = getProperty(GoogleSearch.GOOGLE_CSE_KEY_PROP) val cseKey = getProperty(GoogleSearch.GOOGLE_CSE_KEY_PROP)
try { try {
var messages = searchGoogle("mobitopia", apiKey, cseKey) var messages = searchGoogle("mobitopia", apiKey, cseKey)
assertThat(messages).describedAs("mobitopia results not empty").isNotEmpty assertThat(messages, "mobitopia results not empty").isNotEmpty()
assertThat(messages[0].msg).describedAs("found mobibtopia").containsIgnoringCase("mobitopia") assertThat(messages[0].msg, "found mobibtopia").contains("mobitopia", true)
messages = searchGoogle("aapl", apiKey, cseKey) messages = searchGoogle("aapl", apiKey, cseKey)
assertThat(messages).describedAs("aapl results not empty").isNotEmpty assertThat(messages, "aapl results not empty").isNotEmpty()
assertThat(messages[0].msg).describedAs("found apple").containsIgnoringCase("apple") assertThat(messages[0].msg, "found apple").contains("apple", true)
assertThatThrownBy { searchGoogle("test", "", "apiKey") }
.describedAs("no API key") messages = searchGoogle("adadflkjl", apiKey, cseKey)
assertThat(messages[0], "not found").all {
isInstanceOf(ErrorMessage::class.java)
prop(Message::msg).isEqualTo("No results found.")
}
assertThat(
searchGoogle("", "apikey", "cssKey").first(),
"empty query"
).isInstanceOf(ErrorMessage::class.java)
assertThat { searchGoogle("test", "", "apiKey") }.isFailure()
.isInstanceOf(ModuleException::class.java).hasNoCause() .isInstanceOf(ModuleException::class.java).hasNoCause()
assertThatThrownBy { searchGoogle("test", "apiKey", "") }
.describedAs("no CSE API key") assertThat { searchGoogle("test", "apiKey", "") }.isFailure()
.isInstanceOf(ModuleException::class.java).hasNoCause() .isInstanceOf(ModuleException::class.java).hasNoCause()
assertThatThrownBy { searchGoogle("", "apikey", "apiKey") }
.describedAs("no query").isInstanceOf(ModuleException::class.java).hasNoCause()
} 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")) {
throw sanitizeException(e, apiKey, cseKey) throw e.sanitize(apiKey, cseKey)
} else { } else {
throw e throw e
} }

View file

@ -31,8 +31,11 @@
*/ */
package net.thauvin.erik.mobibot.modules package net.thauvin.erik.mobibot.modules
import assertk.all
import assertk.assertThat
import assertk.assertions.contains
import assertk.assertions.isNotEmpty
import net.thauvin.erik.mobibot.modules.Joke.Companion.randomJoke import net.thauvin.erik.mobibot.modules.Joke.Companion.randomJoke
import org.assertj.core.api.Assertions.assertThat
import org.testng.annotations.Test import org.testng.annotations.Test
/** /**
@ -42,7 +45,9 @@ class JokeTest {
@Test @Test
@Throws(ModuleException::class) @Throws(ModuleException::class)
fun testRandomJoke() { fun testRandomJoke() {
assertThat(randomJoke().msg).describedAs("randomJoke() > 0").isNotEmpty assertThat(randomJoke().msg, "randomJoke() > 0").all {
assertThat(randomJoke().msg).describedAs("randomJoke()").containsIgnoringCase("chuck") isNotEmpty()
contains("chuck", true)
}
} }
} }

View file

@ -31,9 +31,11 @@
*/ */
package net.thauvin.erik.mobibot.modules package net.thauvin.erik.mobibot.modules
import assertk.assertThat
import assertk.assertions.any
import assertk.assertions.contains
import net.thauvin.erik.mobibot.modules.Lookup.Companion.nslookup import net.thauvin.erik.mobibot.modules.Lookup.Companion.nslookup
import net.thauvin.erik.mobibot.modules.Lookup.Companion.whois import net.thauvin.erik.mobibot.modules.Lookup.Companion.whois
import org.assertj.core.api.Assertions.assertThat
import org.testng.annotations.Test import org.testng.annotations.Test
/** /**
@ -44,14 +46,13 @@ class LookupTest {
@Throws(Exception::class) @Throws(Exception::class)
fun testLookup() { fun testLookup() {
val result = nslookup("apple.com") val result = nslookup("apple.com")
assertThat(result).describedAs("lookup(apple.com)").contains("17.253.144.10") assertThat(result, "lookup(apple.com)").contains("17.253.144.10")
} }
@Test @Test
@Throws(Exception::class) @Throws(Exception::class)
fun testWhois() { fun testWhois() {
val result = whois("17.178.96.59", Lookup.WHOIS_HOST) val result = whois("17.178.96.59", Lookup.WHOIS_HOST)
assertThat(result).describedAs("whois(17.178.96.59/Apple Inc.") assertThat(result, "whois(17.178.96.59/Apple Inc.").any { it.contains("Apple Inc.") }
.anyMatch { it.contains("Apple Inc.") }
} }
} }

View file

@ -31,8 +31,16 @@
*/ */
package net.thauvin.erik.mobibot.modules package net.thauvin.erik.mobibot.modules
import net.thauvin.erik.mobibot.Sanitize.sanitizeException import assertk.all
import org.assertj.core.api.Assertions.assertThat import assertk.assertThat
import assertk.assertions.contains
import assertk.assertions.doesNotContain
import assertk.assertions.endsWith
import assertk.assertions.hasMessage
import assertk.assertions.isEqualTo
import assertk.assertions.isNotNull
import assertk.assertions.isNull
import net.thauvin.erik.mobibot.ExceptionSanitizer.sanitize
import org.testng.annotations.DataProvider import org.testng.annotations.DataProvider
import org.testng.annotations.Test import org.testng.annotations.Test
import java.io.IOException import java.io.IOException
@ -58,37 +66,41 @@ class ModuleExceptionTest {
@Test(dataProvider = "dp") @Test(dataProvider = "dp")
fun testGetDebugMessage(e: ModuleException) { fun testGetDebugMessage(e: ModuleException) {
assertThat(e.debugMessage).describedAs("get debug message").isEqualTo(debugMessage) assertThat(e.debugMessage, "get debug message").isEqualTo(debugMessage)
} }
@Test(dataProvider = "dp") @Test(dataProvider = "dp")
fun testGetMessage(e: ModuleException) { fun testGetMessage(e: ModuleException) {
assertThat(e).describedAs("get message").hasMessage(message) assertThat(e, "get message").hasMessage(message)
} }
@Test @Test
fun testSanitizeMessage() { fun testSanitizeMessage() {
val apiKey = "1234567890" val apiKey = "1234567890"
var e = ModuleException(debugMessage, message, IOException("URL http://foo.com?apiKey=$apiKey&userID=me")) var e = ModuleException(debugMessage, message, IOException("URL http://foo.com?apiKey=$apiKey&userID=me"))
assertThat(sanitizeException(e, apiKey, "", "me")).describedAs("sanitized url") assertThat(e.sanitize(apiKey, "", "me").message, "sanitized url").isNotNull().all {
.hasMessageContainingAll("xxxxxxxxxx", "userID=xx", "java.io.IOException") contains("xxxxxxxxxx", "userID=xx", "java.io.IOException")
.hasMessageNotContainingAny(apiKey, "me") doesNotContain(apiKey, "me")
}
e = ModuleException(debugMessage, message, null) e = ModuleException(debugMessage, message, null)
assertThat(sanitizeException(e, apiKey)).describedAs("no cause").hasMessage(message) assertThat(e.sanitize(apiKey), "no cause").hasMessage(message)
e = ModuleException(debugMessage, message, IOException()) e = ModuleException(debugMessage, message, IOException())
assertThat(sanitizeException(e, apiKey)).describedAs("no cause message").hasMessage(message) assertThat(e.sanitize(apiKey), "no cause message").hasMessage(message)
e = ModuleException(apiKey) e = ModuleException(apiKey)
assertThat(sanitizeException(e, apiKey)).describedAs("api key in message").hasMessageNotContaining(apiKey) assertThat(e.sanitize(apiKey).message, "api key in message").isNotNull().doesNotContain(apiKey)
val msg: String? = null val msg: String? = null
e = ModuleException(debugMessage, msg, IOException(msg)) e = ModuleException(debugMessage, msg, IOException(msg))
assertThat(sanitizeException(e, apiKey).message).describedAs("null message").isNull() assertThat(e.sanitize(apiKey).message, "null message").isNull()
e = ModuleException(msg, msg, IOException("foo is $apiKey")) e = ModuleException(msg, msg, IOException("foo is $apiKey"))
assertThat(sanitizeException(e, " ", apiKey, "foo").message).describedAs("key in cause") assertThat(e.sanitize(" ", apiKey, "foo").message, "key in cause").isNotNull().all {
.doesNotContain(apiKey).endsWith("xxx is xxxxxxxxxx") doesNotContain(apiKey)
endsWith("xxx is xxxxxxxxxx")
}
assertThat(e.sanitize(), "empty").isEqualTo(e)
} }
} }

View file

@ -31,8 +31,10 @@
*/ */
package net.thauvin.erik.mobibot.modules package net.thauvin.erik.mobibot.modules
import assertk.assertThat
import assertk.assertions.contains
import assertk.assertions.isNotEmpty
import net.thauvin.erik.mobibot.modules.Ping.Companion.randomPing import net.thauvin.erik.mobibot.modules.Ping.Companion.randomPing
import org.assertj.core.api.Assertions.assertThat
import org.testng.annotations.Test import org.testng.annotations.Test
/** /**
@ -41,13 +43,13 @@ import org.testng.annotations.Test
class PingTest { class PingTest {
@Test @Test
fun testPingsArray() { fun testPingsArray() {
assertThat(Ping.PINGS).describedAs("Pings array is not empty.").isNotEmpty assertThat(Ping.PINGS, "Pings array is not empty.").isNotEmpty()
} }
@Test @Test
fun testRandomPing() { fun testRandomPing() {
for (i in 0..9) { for (i in 0..9) {
assertThat(randomPing()).describedAs("Random ping $i").isIn(Ping.PINGS) assertThat(Ping.PINGS, "Random ping $i").contains(randomPing())
} }
} }
} }

View file

@ -32,23 +32,19 @@
package net.thauvin.erik.mobibot.modules package net.thauvin.erik.mobibot.modules
import org.assertj.core.api.Assertions.assertThat import assertk.assertThat
import assertk.assertions.isEqualTo
import org.testng.annotations.Test import org.testng.annotations.Test
class RockPaperScissorsTest { class RockPaperScissorsTest {
@Test @Test
fun testWinLoseOrDraw() { fun testWinLoseOrDraw() {
assertThat(RockPaperScissors.winLoseOrDraw("scissors", "paper")).describedAs("scissors vs. paper") assertThat(RockPaperScissors.winLoseOrDraw("scissors", "paper"), "scissors vs. paper").isEqualTo("win")
.isEqualTo("win") assertThat(RockPaperScissors.winLoseOrDraw("paper", "rock"), "paper vs. rock").isEqualTo("win")
assertThat(RockPaperScissors.winLoseOrDraw("paper", "rock")).describedAs("paper vs. rock").isEqualTo("win") assertThat(RockPaperScissors.winLoseOrDraw("rock", "scissors"), "rock vs. scissors").isEqualTo("win")
assertThat(RockPaperScissors.winLoseOrDraw("rock", "scissors")).describedAs("rock vs. scissors") assertThat(RockPaperScissors.winLoseOrDraw("paper", "scissors"), "paper vs. scissors").isEqualTo("lose")
.isEqualTo("win") assertThat(RockPaperScissors.winLoseOrDraw("rock", "paper"), "rock vs. paper").isEqualTo("lose")
assertThat(RockPaperScissors.winLoseOrDraw("paper", "scissors")).describedAs("paper vs. scissors") assertThat(RockPaperScissors.winLoseOrDraw("scissors", "rock"), "scissors vs. rock").isEqualTo("lose")
.isEqualTo("lose") assertThat(RockPaperScissors.winLoseOrDraw("scissors", "scissors"), "scissors vs. scissors").isEqualTo("draw")
assertThat(RockPaperScissors.winLoseOrDraw("rock", "paper")).describedAs("rock vs. paper").isEqualTo("lose")
assertThat(RockPaperScissors.winLoseOrDraw("scissors", "rock")).describedAs("scissors vs. rock")
.isEqualTo("lose")
assertThat(RockPaperScissors.winLoseOrDraw("scissors", "scissors"))
.describedAs("scissors vs. scissors").isEqualTo("draw")
} }
} }

View file

@ -31,11 +31,20 @@
*/ */
package net.thauvin.erik.mobibot.modules package net.thauvin.erik.mobibot.modules
import assertk.all
import assertk.assertThat
import assertk.assertions.hasNoCause
import assertk.assertions.isEqualTo
import assertk.assertions.isFailure
import assertk.assertions.isInstanceOf
import assertk.assertions.isNotEmpty
import assertk.assertions.matches
import assertk.assertions.prop
import net.thauvin.erik.mobibot.ExceptionSanitizer.sanitize
import net.thauvin.erik.mobibot.LocalProperties import net.thauvin.erik.mobibot.LocalProperties
import net.thauvin.erik.mobibot.Sanitize.sanitizeException
import net.thauvin.erik.mobibot.modules.StockQuote.Companion.getQuote import net.thauvin.erik.mobibot.modules.StockQuote.Companion.getQuote
import org.assertj.core.api.Assertions.assertThat import net.thauvin.erik.mobibot.msg.ErrorMessage
import org.assertj.core.api.Assertions.assertThatThrownBy import net.thauvin.erik.mobibot.msg.Message
import org.testng.annotations.Test import org.testng.annotations.Test
/** /**
@ -52,24 +61,25 @@ class StockQuoteTest : LocalProperties() {
val apiKey = getProperty(StockQuote.ALPHAVANTAGE_API_KEY_PROP) val apiKey = getProperty(StockQuote.ALPHAVANTAGE_API_KEY_PROP)
try { try {
val messages = getQuote("apple inc", apiKey) val messages = getQuote("apple inc", apiKey)
assertThat(messages).describedAs("response not empty").isNotEmpty assertThat(messages, "response not empty").isNotEmpty()
assertThat(messages[0].msg).describedAs("same stock symbol").matches("Symbol: AAPL .*") assertThat(messages[0].msg, "same stock symbol").matches("Symbol: AAPL .*".toRegex())
assertThat(messages[1].msg).describedAs("price label").matches(buildMatch("Price")) assertThat(messages[1].msg, "price label").matches(buildMatch("Price").toRegex())
assertThat(messages[2].msg).describedAs("previous label").matches(buildMatch("Previous")) assertThat(messages[2].msg, "previous label").matches(buildMatch("Previous").toRegex())
assertThat(messages[3].msg).describedAs("open label").matches(buildMatch("Open")) assertThat(messages[3].msg, "open label").matches(buildMatch("Open").toRegex())
try {
getQuote("blahfoo", apiKey) assertThat(getQuote("blahfoo", apiKey).first(), "invalid symbol").all {
} catch (e: ModuleException) { isInstanceOf(ErrorMessage::class.java)
assertThat(e.message).describedAs("invalid symbol").containsIgnoringCase(StockQuote.INVALID_SYMBOL) prop(Message::msg).isEqualTo(StockQuote.INVALID_SYMBOL)
} }
assertThatThrownBy { getQuote("test", "") }.describedAs("no API key") assertThat(getQuote("", "apikey").first(), "empty symbol").all {
.isInstanceOf(ModuleException::class.java).hasNoCause() isInstanceOf(ErrorMessage::class.java)
assertThatThrownBy { getQuote("", "apikey") }.describedAs("no symbol") prop(Message::msg).isEqualTo(StockQuote.INVALID_SYMBOL)
.isInstanceOf(ModuleException::class.java).hasNoCause() }
assertThat { getQuote("test", "") }.isFailure().isInstanceOf(ModuleException::class.java).hasNoCause()
} 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")) {
throw sanitizeException(e, apiKey) throw e.sanitize(apiKey)
} else { } else {
throw e throw e
} }

View file

@ -31,9 +31,11 @@
*/ */
package net.thauvin.erik.mobibot.modules package net.thauvin.erik.mobibot.modules
import assertk.assertThat
import assertk.assertions.isEqualTo
import assertk.assertions.isSuccess
import net.thauvin.erik.mobibot.LocalProperties import net.thauvin.erik.mobibot.LocalProperties
import net.thauvin.erik.mobibot.modules.Twitter.Companion.twitterPost import net.thauvin.erik.mobibot.modules.Twitter.Companion.twitterPost
import org.assertj.core.api.Assertions.assertThat
import org.testng.annotations.Test import org.testng.annotations.Test
import java.net.InetAddress import java.net.InetAddress
import java.net.UnknownHostException import java.net.UnknownHostException
@ -56,7 +58,7 @@ class TwitterTest : LocalProperties() {
@Throws(ModuleException::class) @Throws(ModuleException::class)
fun testPostTwitter() { fun testPostTwitter() {
val msg = "Testing Twitter API from $ci" val msg = "Testing Twitter API from $ci"
assertThat( assertThat {
twitterPost( twitterPost(
getProperty(Twitter.CONSUMER_KEY_PROP), getProperty(Twitter.CONSUMER_KEY_PROP),
getProperty(Twitter.CONSUMER_SECRET_PROP), getProperty(Twitter.CONSUMER_SECRET_PROP),
@ -65,7 +67,7 @@ class TwitterTest : LocalProperties() {
getProperty(Twitter.HANDLE_PROP), getProperty(Twitter.HANDLE_PROP),
msg, msg,
true true
).msg )
).describedAs("twitterPost($msg)").isEqualTo(msg) }.isSuccess().isEqualTo(msg)
} }
} }

View file

@ -31,6 +31,16 @@
*/ */
package net.thauvin.erik.mobibot.modules package net.thauvin.erik.mobibot.modules
import assertk.all
import assertk.assertThat
import assertk.assertions.contains
import assertk.assertions.endsWith
import assertk.assertions.hasNoCause
import assertk.assertions.isEqualTo
import assertk.assertions.isFailure
import assertk.assertions.isInstanceOf
import assertk.assertions.isNotNull
import assertk.assertions.isTrue
import net.aksingh.owmjapis.api.APIException import net.aksingh.owmjapis.api.APIException
import net.aksingh.owmjapis.core.OWM import net.aksingh.owmjapis.core.OWM
import net.thauvin.erik.mobibot.LocalProperties import net.thauvin.erik.mobibot.LocalProperties
@ -39,8 +49,6 @@ import net.thauvin.erik.mobibot.modules.Weather2.Companion.ftoC
import net.thauvin.erik.mobibot.modules.Weather2.Companion.getCountry import net.thauvin.erik.mobibot.modules.Weather2.Companion.getCountry
import net.thauvin.erik.mobibot.modules.Weather2.Companion.getWeather import net.thauvin.erik.mobibot.modules.Weather2.Companion.getWeather
import net.thauvin.erik.mobibot.modules.Weather2.Companion.mphToKmh import net.thauvin.erik.mobibot.modules.Weather2.Companion.mphToKmh
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.testng.annotations.Test import org.testng.annotations.Test
import kotlin.random.Random import kotlin.random.Random
@ -51,50 +59,63 @@ class Weather2Test : LocalProperties() {
@Test @Test
fun testFtoC() { fun testFtoC() {
val t = ftoC(32.0) val t = ftoC(32.0)
assertThat(t.second).describedAs("32 °F is 0 °C").isEqualTo(0) assertThat(t.second, "32 °F is 0 °C").isEqualTo(0)
} }
@Test @Test
fun testGetCountry() { fun testGetCountry() {
assertThat(getCountry("foo")).describedAs("not a country").isEqualTo(OWM.Country.UNITED_STATES) assertThat(getCountry("foo"), "not a country").isEqualTo(OWM.Country.UNITED_STATES)
assertThat(getCountry("fr")).describedAs("fr is france").isEqualTo(OWM.Country.FRANCE) assertThat(getCountry("fr"), "fr is france").isEqualTo(OWM.Country.FRANCE)
val country = OWM.Country.values() val country = OWM.Country.values()
repeat(3) { repeat(3) {
val rand = country[Random.nextInt(0, country.size - 1)] val rand = country[Random.nextInt(0, country.size - 1)]
assertThat(getCountry(rand.value)).describedAs(rand.name).isEqualTo(rand) assertThat(getCountry(rand.value), rand.name).isEqualTo(rand)
} }
} }
@Test @Test
fun testMphToKmh() { fun testMphToKmh() {
val w = mphToKmh(0.62) val w = mphToKmh(0.62)
assertThat(w.second).describedAs("0.62 mph is 1 km/h").isEqualTo(1) assertThat(w.second, "0.62 mph is 1 km/h").isEqualTo(1)
} }
@Test @Test
@Throws(ModuleException::class) @Throws(ModuleException::class)
fun testWeather() { fun testWeather() {
var messages = getWeather("98204", getProperty(OWM_API_KEY_PROP)) var messages = getWeather("98204", getProperty(OWM_API_KEY_PROP))
assertThat(messages[0].msg).describedAs("is Everett").contains("Everett, United States").contains("US") assertThat(messages[0].msg, "is Everett").all {
assertThat(messages[messages.size - 1].msg).describedAs("is Everett zip code").endsWith("98204%2CUS") contains("Everett, United States")
contains("US")
}
assertThat(messages[messages.size - 1].msg, "is Everett zip code").endsWith("98204%2CUS")
messages = getWeather("San Francisco", getProperty(OWM_API_KEY_PROP)) messages = getWeather("San Francisco", getProperty(OWM_API_KEY_PROP))
assertThat(messages[0].msg).describedAs("is San Francisco").contains("San Francisco").contains("US") assertThat(messages[0].msg, "is San Francisco").all {
assertThat(messages[messages.size - 1].msg).describedAs("is San Fran city code").endsWith("5391959") contains("San Francisco")
contains("US")
}
assertThat(messages[messages.size - 1].msg, "is San Fran city code").endsWith("5391959")
messages = getWeather("London, GB", getProperty(OWM_API_KEY_PROP)) messages = getWeather("London, GB", getProperty(OWM_API_KEY_PROP))
assertThat(messages[0].msg).describedAs("is UK").contains("London, United Kingdom").contains("GB") assertThat(messages[0].msg, "is UK").all {
assertThat(messages[messages.size - 1].msg).describedAs("is London city code").endsWith("2643743") contains("London, United Kingdom")
contains("GB")
}
assertThat(messages[messages.size - 1].msg, "is London city code").endsWith("2643743")
assertThatThrownBy { getWeather("Foo, US", getProperty(OWM_API_KEY_PROP)) } try {
.describedAs("foo not found").hasCauseInstanceOf(APIException::class.java) getWeather("Foo, US", getProperty(OWM_API_KEY_PROP))
assertThatThrownBy { getWeather("test", "") } } catch (e: ModuleException) {
.describedAs("no API key").isInstanceOf(ModuleException::class.java).hasNoCause() assertThat(e.cause, "cause is API exception").isNotNull().isInstanceOf(APIException::class.java)
assertThatThrownBy { getWeather("test", null) } }
.describedAs("null API key").isInstanceOf(ModuleException::class.java).hasNoCause()
assertThat { getWeather("test", "") }.isFailure()
.isInstanceOf(ModuleException::class.java).hasNoCause()
assertThat { getWeather("test", null) }.isFailure()
.isInstanceOf(ModuleException::class.java).hasNoCause()
messages = getWeather("", "apikey") messages = getWeather("", "apikey")
assertThat(messages[0].isError).describedAs("no query").isTrue assertThat(messages[0].isError, "no query").isTrue()
} }
} }

View file

@ -31,14 +31,18 @@
*/ */
package net.thauvin.erik.mobibot.modules package net.thauvin.erik.mobibot.modules
import assertk.assertThat
import assertk.assertions.endsWith
import assertk.assertions.isSuccess
import assertk.assertions.matches
import assertk.assertions.startsWith
import net.thauvin.erik.mobibot.Utils.bold import net.thauvin.erik.mobibot.Utils.bold
import net.thauvin.erik.mobibot.modules.WorldTime.Companion.BEATS_KEYWORD import net.thauvin.erik.mobibot.modules.WorldTime.Companion.BEATS_KEYWORD
import net.thauvin.erik.mobibot.modules.WorldTime.Companion.COUNTRIES_MAP import net.thauvin.erik.mobibot.modules.WorldTime.Companion.COUNTRIES_MAP
import net.thauvin.erik.mobibot.modules.WorldTime.Companion.time import net.thauvin.erik.mobibot.modules.WorldTime.Companion.time
import org.assertj.core.api.Assertions.assertThat import org.pircbotx.Colors
import org.testng.annotations.Test import org.testng.annotations.Test
import java.time.ZoneId import java.time.ZoneId
import java.time.zone.ZoneRulesException
/** /**
* The `WordTimeTest` class. * The `WordTimeTest` class.
@ -46,16 +50,23 @@ import java.time.zone.ZoneRulesException
class WordTimeTest { class WordTimeTest {
@Test @Test
fun testTime() { fun testTime() {
assertThat(time("PST").msg).describedAs("PST").endsWith(bold("Los Angeles")) assertThat(time(), "no zone").matches(
assertThat(time("BLAH").isError).describedAs("BLAH").isTrue ("The time is ${Colors.BOLD}\\d{1,2}:\\d{2}${Colors.BOLD} " +
assertThat(time("BEAT").msg).describedAs(BEATS_KEYWORD).matches("[\\w ]+: .?@\\d{3}+.? .beats") "on ${Colors.BOLD}\\w+, \\d{1,2} \\w+ \\d{4}${Colors.BOLD} " +
"in ${Colors.BOLD}Los Angeles${Colors.BOLD}").toRegex()
)
assertThat(time(""), "empty zone").endsWith(bold("Los Angeles"))
assertThat(time("PST"), "PST").endsWith(bold("Los Angeles"))
assertThat(time("GB"), "GB").endsWith(bold("London"))
assertThat(time("FR"), "FR").endsWith(bold("Paris"))
assertThat(time("BLAH"), "BLAH").startsWith("Unsupported")
assertThat(time("BEAT"), BEATS_KEYWORD).matches("[\\w ]+ .?@\\d{3}+.? .beats".toRegex())
} }
@Test @Test
@Throws(ZoneRulesException::class) fun testZones() {
fun testCountries() {
COUNTRIES_MAP.filter { it.value != BEATS_KEYWORD }.forEach { COUNTRIES_MAP.filter { it.value != BEATS_KEYWORD }.forEach {
ZoneId.of(it.value) assertThat { ZoneId.of(it.value) }.isSuccess()
} }
} }
} }

View file

@ -32,7 +32,8 @@
package net.thauvin.erik.mobibot.msg package net.thauvin.erik.mobibot.msg
import org.assertj.core.api.Assertions.assertThat import assertk.assertThat
import assertk.assertions.isTrue
import org.testng.annotations.Test import org.testng.annotations.Test
class TestMessage { class TestMessage {
@ -41,15 +42,15 @@ class TestMessage {
var msg = Message() var msg = Message()
msg.isError = true msg.isError = true
assertThat(msg.isNotice).describedAs("message is notice").isTrue assertThat(msg.isNotice, "message is notice").isTrue()
msg = Message("foo", isError = true) msg = Message("foo", isError = true)
assertThat(msg.isNotice).describedAs("message is notice too").isTrue assertThat(msg.isNotice, "message is notice too").isTrue()
} }
@Test @Test
fun testErrorMessage() { fun testErrorMessage() {
val msg = ErrorMessage("foo") val msg = ErrorMessage("foo")
assertThat(msg.isNotice).describedAs("error message is notice").isTrue assertThat(msg.isNotice, "error message is notice").isTrue()
} }
} }

View file

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" version="2.0">
<channel>
<title>#mobibot IRC Links</title>
<link>https://www.mobitopia.org/mobibot/logs</link>
<description>Links from irc.example.com on #mobibot</description>
<language>en</language>
<pubDate>Sun, 31 Oct 2021 21:45:11 GMT</pubDate>
<dc:date>2021-10-31T21:45:11Z</dc:date>
<dc:language>en</dc:language>
<item>
<title>Example 2</title>
<link>https://www.example.com/2</link>
<description>Posted by &lt;b&gt;Skynx&lt;/b&gt; on &lt;a href="irc://irc.libera.chat/#mobibot"&gt;&lt;b&gt;#mobibot&lt;/b&gt;&lt;/a&gt;</description>
<category>tag2-1</category>
<category>tag2-2</category>
<pubDate>Sun, 31 Oct 2021 21:45:11 GMT</pubDate>
<guid>https://www.foo.com</guid>
<dc:creator>mobibot@irc.libera.chat (Skynx)</dc:creator>
<dc:date>2021-10-31T00:01:00Z</dc:date>
</item>
<item>
<title>Example 1</title>
<link>https://www.example.com/1</link>
<description>Posted by &lt;b&gt;ErikT&lt;/b&gt; on &lt;a href="irc://irc.libera.chat/#mobibot"&gt;&lt;b&gt;#mobibot&lt;/b&gt;&lt;/a&gt;
&lt;br/&gt;&lt;br/&gt;ErikT: This is comment 1. &lt;br/&gt;Skynx: This is comment 2.
</description>
<category>tag1-1</category>
<category>tag1-2</category>
<pubDate>Sun, 31 Oct 2021 21:43:15 GMT</pubDate>
<guid>https://www.example.com/</guid>
<dc:creator>mobibot@irc.libera.chat (ErikT)</dc:creator>
<dc:date>2021-10-31T00:00:00Z</dc:date>
</item>
</channel>
</rss>

View file

@ -1,9 +1,9 @@
#Generated by the Semver Plugin for Gradle #Generated by the Semver Plugin for Gradle
#Wed Sep 15 17:27:25 PDT 2021 #Mon Nov 08 13:50:12 PST 2021
version.buildmeta=1400 version.buildmeta=2220
version.major=0 version.major=0
version.minor=8 version.minor=8
version.patch=0 version.patch=0
version.prerelease=beta version.prerelease=beta
version.project=mobibot version.project=mobibot
version.semver=0.8.0-beta+1400 version.semver=0.8.0-beta+2220