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="url" value="file:$MAVEN_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>
</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 'com.github.ben-manes.versions' version '0.39.0'
id 'idea'
id 'io.gitlab.arturbosch.detekt' version '1.18.1'
id 'io.gitlab.arturbosch.detekt' version '1.19.0-RC1'
id 'jacoco'
id 'java'
id 'net.thauvin.erik.gradle.semver' version '1.0.4'
id 'org.jetbrains.kotlin.jvm' version '1.6.0-RC'
id 'org.jetbrains.kotlin.kapt' version '1.6.0-RC'
id 'org.jetbrains.kotlin.jvm' version '1.6.0-RC2'
id 'org.jetbrains.kotlin.kapt' version '1.6.0-RC2'
id 'org.sonarqube' version '3.3'
id 'pmd'
}
@ -22,12 +22,13 @@ mainClassName = packageName + '.Mobibot'
ext.versions = [
log4j: '2.14.1',
pmd : '6.35.0',
pmd : '6.40.0',
]
repositories {
mavenLocal()
mavenCentral()
maven { url 'https://jitpack.io' }
maven { url 'https://oss.sonatype.org/content/repositories/snapshots' }
}
@ -35,8 +36,13 @@ dependencies {
kapt(semverProcessor)
compileOnly(semverProcessor)
implementation 'pircbot:pircbot:1.5.0'
compileOnly 'pircbot:pircbot:1.5.0:sources'
implementation 'com.github.pircbotx:pircbotx:-SNAPSHOT'
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 '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-core:$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 '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.objecthunter:exp4j:0.4.8'
@ -59,7 +66,9 @@ dependencies {
implementation 'org.jsoup:jsoup:1.14.3'
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'
}
@ -117,6 +126,7 @@ jar {
manifest.attributes('Main-Class': mainClassName,
'Class-Path': '. ./lib/' + configurations.runtimeClasspath.collect { it.getName() }.join(' ./lib/'))
archiveVersion.set("")
exclude('log4j2.xml')
}
clean {

View file

@ -2,54 +2,76 @@
<SmellBaseline>
<ManuallySuppressedIssues></ManuallySuppressedIssues>
<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>LongMethod:EntriesMgr.kt$EntriesMgr$ fun saveEntries( bot: Mobibot, entries: List&lt;EntryLink&gt;, history: MutableList&lt;String&gt;, isDayBackup: Boolean )</ID>
<ID>LongMethod:Mobibot.kt$Mobibot.Companion$ @JvmStatic fun main(args: Array&lt;String&gt;)</ID>
<ID>LongMethod:FeedsMgr.kt$FeedsMgr.Companion$ fun saveFeed(entries: Entries, currentFile: String = currentXml)</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: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$( 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:Comment.kt$Comment$( channel: String, cmd: String, entry: EntryLink, entryIndex: Int, commentIndex: Int, event: GenericMessageEvent )</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:Calc.kt$Calc$override fun commandResponse( sender: String, cmd: String, args: String, isPrivate: Boolean )</ID>
<ID>NestedBlockDepth:Comment.kt$Comment$override fun commandResponse( sender: String, login: String, args: String, isOp: Boolean, isPrivate: Boolean )</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:Comment.kt$Comment$override fun commandResponse(channel: String, args: String, event: GenericMessageEvent)</ID>
<ID>NestedBlockDepth:CurrencyConverter.kt$CurrencyConverter.Companion$ @JvmStatic fun convertCurrency(query: String): Message</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:LinksMgr.kt$LinksMgr$override fun commandResponse( sender: String, login: String, args: String, isOp: Boolean, isPrivate: Boolean )</ID>
<ID>NestedBlockDepth:Lookup.kt$Lookup$override fun commandResponse( sender: String, cmd: String, args: String, isPrivate: Boolean )</ID>
<ID>NestedBlockDepth:Mobibot.kt$Mobibot$ fun connect()</ID>
<ID>NestedBlockDepth:StockQuote.kt$StockQuote$ 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:FeedsMgr.kt$FeedsMgr.Companion$ fun saveFeed(entries: Entries, currentFile: String = currentXml)</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:LinksMgr.kt$LinksMgr$override fun commandResponse(channel: String, args: String, event: GenericMessageEvent)</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:Tell.kt$Tell$ @JvmOverloads fun send(nickname: String, isMessage: Boolean = false)</ID>
<ID>NestedBlockDepth:Tell.kt$Tell$// Delete message. private fun deleteMessage(sender: String, args: String, isOp: Boolean, isPrivate: Boolean)</ID>
<ID>NestedBlockDepth:TellMessagesMgr.kt$TellMessagesMgr$ fun save(file: String, messages: List&lt;TellMessage?&gt;?, logger: Logger)</ID>
<ID>NestedBlockDepth:Tell.kt$Tell$ fun send(event: GenericUserEvent)</ID>
<ID>NestedBlockDepth:Tell.kt$Tell$// Delete message. private fun deleteMessage(channel: String, args: String, event: GenericMessageEvent)</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: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:WorldTime.kt$WorldTime$override fun commandResponse( sender: String, cmd: String, args: String, isPrivate: 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(sender: String, topic: String, isOp: Boolean, isPrivate: Boolean): 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 help(channel: String, topic: String, event: GenericMessageEvent): 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: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: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:Weather2.kt$Weather2.Companion$e: NullPointerException</ID>
<ID>TooManyFunctions:Mobibot.kt$Mobibot : PircBot</ID>
<ID>TooManyFunctions:Tell.kt$Tell : AbstractCommand</ID>
</CurrentIssues>
</SmellBaseline>

View file

@ -1,16 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
<Configuration status="warn">
<Appenders>
<Console name="stderr" target="SYSTEM_ERR">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
</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>
<Loggers>
<Logger name="net.thauvin.erik.mobibot" level="warn" additivity="false">
<AppenderRef ref="stderr"/>
</Logger>
<Root level="error">
<Root level="warn" additivity="false">
<AppenderRef ref="stderr"/>
</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>
</Configuration>

View file

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

View file

@ -32,9 +32,9 @@
package net.thauvin.erik.mobibot.modules;
import net.thauvin.erik.mobibot.Mobibot;
import net.thauvin.erik.mobibot.Utils;
import org.jetbrains.annotations.NotNull;
import org.pircbotx.hooks.types.GenericMessageEvent;
import java.security.SecureRandom;
@ -66,8 +66,8 @@ public final class War extends AbstractModule {
/**
* The default constructor.
*/
public War(final Mobibot bot) {
super(bot);
public War() {
super();
commands.add(WAR_CMD);
@ -79,10 +79,8 @@ public final class War extends AbstractModule {
* {@inheritDoc}
*/
@Override
public void commandResponse(@NotNull final String sender,
@NotNull final String cmd,
@NotNull final String args,
final boolean isPrivate) {
public void commandResponse(@NotNull final String channel, @NotNull final String cmd, @NotNull final String args,
@NotNull final GenericMessageEvent event) {
int i;
int y;
@ -90,20 +88,20 @@ public final class War extends AbstractModule {
i = RANDOM.nextInt(HEARTS.length);
y = RANDOM.nextInt(HEARTS.length);
getBot().send(sender + " drew: " + DECK[RANDOM.nextInt(DECK.length)][i]);
getBot().action("drew: " + DECK[RANDOM.nextInt(DECK.length)][y]);
event.respond("you drew " + DECK[RANDOM.nextInt(DECK.length)][i]);
event.getBot().sendIRC().action(channel, "drew " + DECK[RANDOM.nextInt(DECK.length)][y]);
if (i != y) {
break;
}
getBot().send("This means " + bold("WAR") + '!');
event.respond("This means " + bold("WAR") + '!');
}
if (i < y) {
getBot().action("lost.");
event.getBot().sendIRC().action(channel, "lost.");
} 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.modules.AbstractModule
import org.pircbotx.hooks.events.PrivateMessageEvent
import org.pircbotx.hooks.types.GenericMessageEvent
import java.util.Properties
/**
@ -77,7 +79,7 @@ class Addons {
if (isEnabled()) {
commands.add(this)
if (isVisible) {
if (isOp) {
if (isOpOnly) {
ops.add(name)
} else {
names.add(name)
@ -90,17 +92,18 @@ class Addons {
/**
* Execute a command or module.
*/
fun exec(sender: String, login: String, cmd: String, args: String, isOp: Boolean, isPrivate: Boolean): Boolean {
for (command in commands) {
fun exec(channel: String, cmd: String, args: String, event: GenericMessageEvent): Boolean {
val cmds = if (event is PrivateMessageEvent) commands else commands.filter { it.isPublic }
for (command in cmds) {
if (command.name.startsWith(cmd)) {
command.commandResponse(sender, login, args, isOp, isPrivate)
command.commandResponse(channel, args, event)
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) {
if (module.commands.contains(cmd)) {
module.commandResponse(sender, cmd, args, isPrivate)
module.commandResponse(channel, cmd, args, event)
return true
}
}
@ -110,10 +113,10 @@ class Addons {
/**
* 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) {
if (command.matches(message)) {
command.commandResponse(sender, login, message, isOp, isPrivate)
if (command.matches(event.message)) {
command.commandResponse(channel, event.message, event)
return true
}
}
@ -123,15 +126,15 @@ class Addons {
/**
* 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) {
if (command.isVisible && command.name.startsWith(topic)) {
return command.helpResponse(topic, sender, isOp, isPrivate)
return command.helpResponse(channel, topic, event)
}
}
for (module in modules) {
if (module.commands.contains(topic)) {
return module.helpResponse(sender, isPrivate)
return module.helpResponse(event)
}
}
return false

View file

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

View file

@ -36,38 +36,36 @@ import com.rometools.rome.io.SyndFeedInput
import com.rometools.rome.io.XmlReader
import net.thauvin.erik.mobibot.Utils.green
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.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.net.URL
/**
* Reads an RSS feed.
*/
class FeedReader(
// Bot
private val bot: Mobibot,
// Nick of the person who sent the message
private val sender: String,
// URL to fetch
private val url: String
) : Runnable {
class FeedReader(private val url: String, val event: GenericMessageEvent) : Runnable {
private val logger: Logger = LoggerFactory.getLogger(FeedsMgr::class.java)
/**
* Fetches the Feed's items.
*/
override fun run() {
with(bot) {
try {
readFeed(url).forEach {
send(sender, it)
}
} catch (e: FeedException) {
if (logger.isDebugEnabled) logger.debug("Unable to parse the feed at $url", e)
send(sender, "An error has occurred while parsing the feed: ${e.message}", false)
} catch (e: IOException) {
if (logger.isDebugEnabled) logger.debug("Unable to fetch the feed at $url", e)
send(sender, "An error has occurred while fetching the feed: ${e.message}", false)
try {
readFeed(url).forEach {
event.sendMessage("", it)
}
} catch (e: FeedException) {
if (logger.isWarnEnabled) logger.warn("Unable to parse the feed at $url", e)
event.sendMessage("An error has occurred while parsing the feed: ${e.message}")
} catch (e: IOException) {
if (logger.isWarnEnabled) logger.warn("Unable to fetch the feed at $url", e)
event.sendMessage("An error has occurred while fetching the feed: ${e.message}")
}
}
@ -81,11 +79,11 @@ class FeedReader(
val feed = input.build(reader)
val items = feed.entries
if (items.isEmpty()) {
messages.add(PublicMessage("There is currently nothing to view."))
messages.add(NoticeMessage("There is currently nothing to view."))
} else {
items.take(maxItems).forEach {
messages.add(PublicMessage(it.title))
messages.add(PublicMessage(helpFormat(green(it.link), false)))
messages.add(NoticeMessage(it.title))
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
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.thauvin.erik.mobibot
import net.thauvin.erik.mobibot.Utils.appendIfMissing
import net.thauvin.erik.mobibot.Utils.buildCmdSyntax
import net.thauvin.erik.mobibot.Utils.colorize
import net.thauvin.erik.mobibot.Utils.bot
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.today
import net.thauvin.erik.mobibot.commands.AddLog
import net.thauvin.erik.mobibot.commands.ChannelFeed
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.Ignore
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.Modules
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.View
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.CryptoPrices
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.RockPaperScissors
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.Weather2
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 org.apache.commons.cli.CommandLine
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.Options
import org.apache.commons.cli.ParseException
import org.apache.logging.log4j.Level
import org.apache.logging.log4j.LogManager
import org.apache.logging.log4j.Logger
import org.jibble.pircbot.PircBot
import org.pircbotx.Configuration
import org.pircbotx.PircBotX
import org.pircbotx.hooks.ListenerAdapter
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.File
import java.io.FileNotFoundException
@ -100,427 +102,139 @@ import java.io.PrintStream
import java.nio.file.Files
import java.nio.file.Paths
import java.util.Properties
import java.util.Timer
import java.util.logging.ConsoleHandler
import java.util.regex.Pattern
import kotlin.system.exitProcess
/**
* Implements the #mobitopia bot.
*/
@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
private val addons = Addons()
// Tell module
private val tell: Tell
/** Main channel. */
val channel: String
// IRC port
private val ircPort: Int
/** IRC server. */
val ircServer: String
/** Logger. */
val logger: Logger = LogManager.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)
}
}
val logger: Logger = LoggerFactory.getLogger(Mobibot::class.java)
/**
* Connects to the server and joins the channel.
*/
fun connect() {
try {
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)
}
PircBotX(config).startBot()
}
/**
* Responds with the default help.
*/
@Suppress("MagicNumber")
fun helpDefault(sender: String, isOp: Boolean, isPrivate: Boolean) {
send(sender, "Type a URL on $channel to post it.", isPrivate)
send(sender, "For more information on a specific command, type:", isPrivate)
send(
sender,
helpFormat(buildCmdSyntax("%c ${Constants.HELP_CMD} <command>", nick, isPrivate)),
isPrivate
private fun helpDefault(event: GenericMessageEvent) {
event.sendMessage("Type a URL on $channel to post it.")
event.sendMessage("For more information on a specific command, type:")
event.sendMessage(
Utils.helpFormat(
Utils.buildCmdSyntax(
"%c ${Constants.HELP_CMD} <command>",
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) {
send(sender, "The op commands are:", isPrivate)
sendList(sender, addons.ops, 8, isPrivate = isPrivate, isBold = true, isIndent = true)
event.sendMessage("The commands are:")
event.sendList(addons.names, 8, 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.
*/
private fun helpResponse(sender: String, topic: String, isPrivate: Boolean) {
val isOp = isOp(sender)
if (topic.isBlank() || !addons.help(sender, topic.lowercase().trim(), isOp, isPrivate)) {
helpDefault(sender, isOp, isPrivate)
private fun helpResponse(event: GenericMessageEvent, topic: String) {
if (topic.isBlank() || !addons.help(channel, topic.lowercase().trim(), event)) {
helpDefault(event)
}
}
/**
* Identifies the bot.
*/
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 onAction(event: ActionEvent?) {
if (channel == event?.channel?.name) {
storeRecap(event.user!!.nick, event.action, true)
}
}
/**
* Returns {@code true} if the specified sender is an Op on the [channel][.ircChannel].
*/
fun isOp(sender: String): Boolean {
for (user in getUsers(channel)) {
if (user.nick == sender) {
return user.isOp
override fun onDisconnect(event: DisconnectEvent?) {
with(event!!.getBot<PircBotX>()) {
LinksMgr.twitter.notification("$nick disconnected from irc://$serverHostname")
}
LinksMgr.twitter.shutdown()
}
override fun onPrivateMessage(event: PrivateMessageEvent?) {
if (logger.isTraceEnabled) logger.trace("<<< ${event!!.user!!.nick}: ${event.message}")
val cmds = event!!.message.trim().split(" ".toRegex(), 2)
val cmd = cmds[0].lowercase()
val args = if (cmds.size > 1) {
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 onJoin(event: JoinEvent?) {
with(event!!.getBot<PircBotX>()) {
if (event.user!!.nick == nick) {
LinksMgr.twitter.notification("$nick has joined ${event.channel.name} on irc://$serverHostname")
} else {
tell.send(event)
}
}
return false
}
/**
* Joins the bot's channel.
*/
private fun joinChannel() {
joinChannel(channel)
twitter.notification("$name ${ReleaseInfo.VERSION} has joined $channel")
}
override fun onDisconnect() {
if (weblogUrl.isNotBlank()) {
version = weblogUrl
}
@Suppress("MagicNumber")
sleep(5)
connect()
}
override fun onMessage(
channel: String,
sender: String,
login: String,
hostname: String,
message: String
) {
if (logger.isDebugEnabled) logger.debug(">>> $sender: $message")
tell.send(sender, true)
if (message.matches("(?i)${Pattern.quote(nick)}:.*".toRegex())) { // mobibot: <command>
override fun onMessage(event: MessageEvent?) {
val sender = event!!.user!!.nick
val message = event.message
tell.send(event)
if (message.matches("(?i)${Pattern.quote(event.bot().nick)}:.*".toRegex())) { // mobibot: <command>
if (logger.isTraceEnabled) logger.trace(">>> $sender: $message")
val cmds = message.substring(message.indexOf(':') + 1).trim().split(" ".toRegex(), 2)
val cmd = cmds[0].lowercase()
val args = if (cmds.size > 1) {
cmds[1].trim()
} else ""
if (cmd.startsWith(Constants.HELP_CMD)) { // mobibot: help
helpResponse(sender, args, false)
helpResponse(event, args)
} else {
// Execute module or command
addons.exec(sender, login, cmd, args, isOp(sender), false)
addons.exec(channel, cmd, args, event)
}
} else {
// Links, e.g.: https://www.example.com/ or L1: , etc.
addons.match(sender, login, message, isOp(sender), false)
} else if (addons.match(channel, event)) { // Links, e.g.: https://www.example.com/ or L1: , etc.
if (logger.isTraceEnabled) logger.trace(">>> $sender: $message")
}
storeRecap(sender, message, false)
}
override fun onPrivateMessage(
sender: String,
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 onNickChange(event: NickChangeEvent?) {
tell.send(event!!)
}
override fun onAction(sender: String, login: String, hostname: String, target: String, action: String) {
if (channel == target) {
storeRecap(sender, action, true)
}
}
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)
override fun onPart(event: PartEvent?) {
with(event!!.getBot<PircBotX>()) {
if (event.user!!.nick == nick) {
LinksMgr.twitter.notification("$nick has left ${event.channel.name} on irc://$serverHostname")
}
}
}
/**
* 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 {
// Maximum number of times the bot will try to reconnect, if disconnected
private const val MAX_RECONNECT = 10
/**
* The Truth is Out There!
*/
@Throws(Exception::class)
@JvmStatic
fun main(args: Array<String>) {
// Set up the command line options
@ -593,7 +307,7 @@ class Mobibot(nickname: String, channel: String, logsDirPath: String, p: Propert
val stdout = PrintStream(
BufferedOutputStream(
FileOutputStream(
logsDir + channel.substring(1) + '.' + today() + ".log", true
logsDir + channel.substring(1) + '.' + Utils.today() + ".log", true
)
), true
)
@ -615,11 +329,8 @@ class Mobibot(nickname: String, channel: String, logsDirPath: String, p: Propert
}
}
// Create the bot
val bot = Mobibot(nickname, channel, logsDir, p)
// Connect
bot.connect()
// Start the bot
Mobibot(nickname, channel, logsDir, p).connect()
}
}
}
@ -629,97 +340,93 @@ class Mobibot(nickname: String, channel: String, logsDirPath: String, p: Propert
* Initialize the bot.
*/
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
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
loggerLevel = logger.level
// Load the current entries
with(LinksMgr) {
entries.channel = channel
entries.ircServer = ircServer
entries.logsDir = logsDirPath
entries.backlogs = p.getProperty("backlogs", "")
entries.load()
setVerbose(true)
setAutoNickChange(true)
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 up pinboard
pinboard.setApiToken(p.getProperty("pinboard-api-token", ""))
}
// Set the pinboard authentication
setPinboardAuth(p.getProperty("pinboard-api-token", ""))
// Load the commands
addons.add(AddLog(this), p)
addons.add(ChannelFeed(this, channelName), p)
addons.add(Cycle(this), p)
addons.add(Die(this), p)
addons.add(Debug(this), p)
addons.add(Ignore(this), p)
addons.add(Info(this), p)
addons.add(Kill(this), p)
addons.add(Me(this), p)
addons.add(Modules(this), p)
addons.add(Msg(this), p)
addons.add(Nick(this), p)
addons.add(Recap(this), p)
addons.add(Say(this), p)
addons.add(Users(this), p)
addons.add(Versions(this), p)
addons.add(ChannelFeed(channel.removePrefix("#")), p)
addons.add(Comment(), p)
addons.add(Cycle(), p)
addons.add(Die(), p)
addons.add(Ignore(), p)
addons.add(LinksMgr(), p)
addons.add(Me(), p)
addons.add(Msg(), p)
addons.add(Nick(), p)
addons.add(Posting(), p)
addons.add(Recap(), p)
addons.add(Say(), p)
addons.add(Tags(), p)
// Tell command
tell = Tell(this)
tell = Tell("${logsDirPath}${nickname}.ser")
addons.add(tell, p)
// Load the links commands
addons.add(Comment(this), p)
addons.add(Posting(this), p)
addons.add(Tags(this), p)
addons.add(LinksMgr(this), p)
addons.add(View(this), p)
addons.add(LinksMgr.twitter, p)
addons.add(Users(), p)
addons.add(Versions(), p)
addons.add(View(), p)
// Load the modules
addons.add(Calc(this), p)
addons.add(CryptoPrices(this), p)
addons.add(CurrencyConverter(this), p)
addons.add(Dice(this), p)
addons.add(GoogleSearch(this), p)
addons.add(Joke(this), p)
addons.add(Lookup(this), p)
addons.add(Ping(this), p)
addons.add(RockPaperScissors(this), p)
addons.add(StockQuote(this), p)
addons.add(War(this), p)
addons.add(Weather2(this), p)
addons.add(WorldTime(this), p)
// Twitter module
twitter = Twitter(this)
addons.add(twitter, p)
addons.add(Calc(), p)
addons.add(CryptoPrices(), p)
addons.add(CurrencyConverter(), p)
addons.add(Dice(), p)
addons.add(GoogleSearch(), p)
addons.add(Info(tell), p)
addons.add(Joke(), p)
addons.add(Lookup(), p)
addons.add(Modules(addons.modulesNames), p)
addons.add(Ping(), p)
addons.add(RockPaperScissors(), p)
addons.add(StockQuote(), p)
addons.add(Weather2(), p)
addons.add(WorldTime(), p)
addons.add(War(), p)
// Sort the addons
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)
* All rights reserved.
@ -45,33 +45,44 @@ import java.util.Date
/**
* Handles posts to pinboard.in.
*/
object PinboardUtils {
class Pinboard {
private val poster = PinboardPoster()
/**
* Adds a pin.
*/
@JvmStatic
fun addPin(poster: PinboardPoster, ircServer: String, entry: EntryLink) {
runBlocking {
launch {
poster.addPin(
entry.link,
entry.title,
entry.postedBy(ircServer),
entry.pinboardTags,
entry.date.toTimestamp()
)
fun addPin(ircServer: String, entry: EntryLink) {
if (poster.apiToken.isNotBlank()) {
runBlocking {
launch {
poster.addPin(
entry.link,
entry.title,
entry.postedBy(ircServer),
entry.pinboardTags,
entry.date.toTimestamp()
)
}
}
}
}
/**
* Sets the pinboard API token.
*/
fun setApiToken(apiToken: String) {
poster.apiToken = apiToken
}
/**
* Deletes a pin.
*/
@JvmStatic
fun deletePin(poster: PinboardPoster, entry: EntryLink) {
runBlocking {
launch {
poster.deletePin(entry.link)
fun deletePin(entry: EntryLink) {
if (poster.apiToken.isNotBlank()) {
runBlocking {
launch {
poster.deletePin(entry.link)
}
}
}
}
@ -79,30 +90,15 @@ object PinboardUtils {
/**
* Updates a pin.
*/
@JvmStatic
fun updatePin(poster: PinboardPoster, ircServer: String, oldUrl: String, entry: EntryLink) {
runBlocking {
launch {
with(entry) {
if (oldUrl != link) {
poster.deletePin(oldUrl)
poster.addPin(
link,
title,
entry.postedBy(ircServer),
pinboardTags,
date.toTimestamp()
)
} else {
poster.addPin(
link,
title,
entry.postedBy(ircServer),
pinboardTags,
date.toTimestamp(),
replace = true,
shared = true
)
fun updatePin(ircServer: String, oldUrl: String, entry: EntryLink) {
if (poster.apiToken.isNotBlank()) {
runBlocking {
launch {
with(entry) {
if (oldUrl != link) {
poster.deletePin(oldUrl)
}
poster.addPin(link, title, entry.postedBy(ircServer), pinboardTags, date.toTimestamp())
}
}
}
@ -112,8 +108,7 @@ object PinboardUtils {
/**
* Format a date to a UTC timestamp.
*/
@JvmStatic
fun Date.toTimestamp(): String {
private fun Date.toTimestamp(): String {
return ZonedDateTime.ofInstant(
this.toInstant().truncatedTo(ChronoUnit.SECONDS),
ZoneId.systemDefault()

View file

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

View file

@ -32,10 +32,11 @@
package net.thauvin.erik.mobibot
import net.thauvin.erik.mobibot.modules.Twitter
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() {
bot.twitter.postEntry(index)
twitter.postEntry(index)
}
}

View file

@ -31,9 +31,13 @@
*/
package net.thauvin.erik.mobibot
import net.thauvin.erik.mobibot.msg.Message
import net.thauvin.erik.mobibot.msg.Message.Companion.DEFAULT_COLOR
import org.jibble.pircbot.Colors
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.IOException
import java.io.InputStreamReader
@ -85,6 +89,13 @@ object Utils {
@JvmStatic
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
* nick.
@ -159,6 +170,14 @@ object Utils {
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.
*/
@ -203,6 +222,56 @@ object Utils {
@JvmStatic
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.
*/
@ -258,7 +327,6 @@ object Utils {
/**
* Converts milliseconds to year month week day hour and minutes.
*/
@Suppress("MagicNumber")
@JvmStatic
fun uptime(uptime: Long): String {
val info = StringBuilder()

View file

@ -32,31 +32,31 @@
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.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
abstract class AbstractCommand(val bot: Mobibot) {
abstract class AbstractCommand {
abstract val name: String
abstract val help: List<String>
abstract val isOp: Boolean
abstract val isOpOnly: Boolean
abstract val isPublic: Boolean
abstract val isVisible: Boolean
val properties: MutableMap<String, String> = ConcurrentHashMap()
abstract fun commandResponse(
sender: String,
login: String,
args: String,
isOp: Boolean,
isPrivate: Boolean
)
abstract fun commandResponse(channel: String, args: String, event: GenericMessageEvent)
open fun helpResponse(command: String, sender: String, isOp: Boolean, isPrivate: Boolean): Boolean {
if (!this.isOp || this.isOp == isOp) {
open fun helpResponse(channel: String, topic: String, event: GenericMessageEvent): Boolean {
if (!isOpOnly || isOpOnly == isChannelOp(channel, event)) {
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
}

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.runBlocking
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.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 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 isVisible = true
@ -53,22 +54,16 @@ class ChannelFeed(bot: Mobibot, channel: String) : AbstractCommand(bot) {
initProperties(FEED_PROP)
}
override fun commandResponse(
sender: String,
login: String,
args: String,
isOp: Boolean,
isPrivate: Boolean
) {
override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
with(properties[FEED_PROP]) {
if (!isNullOrBlank()) {
runBlocking {
launch {
FeedReader(bot, sender, this@with).run()
FeedReader(this@with, event).run()
}
}
} 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
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.isChannelOp
import org.pircbotx.hooks.types.GenericMessageEvent
class Cycle(bot: Mobibot) : AbstractCommand(bot) {
@Suppress("MagicNumber")
class Cycle : AbstractCommand() {
private val wait = 10
override val name = "cycle"
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 isVisible = true
override fun commandResponse(
sender: String,
login: String,
args: String,
isOp: Boolean,
isPrivate: Boolean
) {
with(bot) {
if (isOp) {
send("$sender has just asked me to leave. I'll be back!")
sleep(wait)
partChannel(channel)
sleep(wait)
joinChannel(channel)
override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
with(event.bot()) {
if (isChannelOp(channel, event)) {
runBlocking {
sendIRC().message(channel, "${event.user.nick} asked me to leave. I'll be back!")
userChannelDao.getChannel(channel).send().part()
delay(wait * 1000L)
sendIRC().joinChannel(channel)
}
} 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
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 help = emptyList<String>()
override val isOp = true
override val isOpOnly = true
override val isPublic = false
override val isVisible = false
override fun commandResponse(
sender: String,
login: String,
args: String,
isOp: Boolean,
isPrivate: Boolean
) {
if (isOp) {
bot.send("$sender has just signed my death sentence.")
bot.shutdown(sender)
override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
with(event.bot()) {
if (isChannelOp(channel, event) && (properties[DIE_PROP].isNullOrBlank() || args == properties[DIE_PROP])) {
sendIRC().message(channel, "${event.user?.nick} has just signed my death sentence.")
stopBotReconnect()
sendIRC().quitServer("The Bot is Out There!")
}
}
}
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
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.buildCmdSyntax
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 org.pircbotx.hooks.types.GenericMessageEvent
class Ignore(bot: Mobibot) : AbstractCommand(bot) {
class Ignore : AbstractCommand() {
private val me = "me"
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> ...]"))
)
override val isOp = false
override val isOpOnly = false
override val isPublic = true
override val isVisible = true
@ -73,56 +77,45 @@ class Ignore(bot: Mobibot) : AbstractCommand(bot) {
}
}
override fun commandResponse(
sender: String,
login: String,
args: String,
isOp: Boolean,
isPrivate: Boolean
) {
override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
val isMe = args.trim().equals(me, true)
if (isMe || !isOp) {
val nick = sender.lowercase()
ignoreNick(bot, nick, isMe, isPrivate)
if (isMe || !isChannelOp(channel, event)) {
val nick = event.user.nick.lowercase()
ignoreNick(nick, isMe, event)
} else {
ignoreOp(bot, sender, args, isPrivate)
ignoreOp(args, event)
}
}
override fun helpResponse(
command: String,
sender: String,
isOp: Boolean,
isPrivate: Boolean
): Boolean {
return if (isOp) {
override fun helpResponse(channel: String, topic: String, event: GenericMessageEvent): Boolean {
return if (isChannelOp(channel, event)) {
for (h in helpOp) {
bot.send(sender, buildCmdSyntax(h, bot.nick, isPrivate), isPrivate)
event.sendMessage(buildCmdSyntax(h, event.bot().nick, true))
}
true
} 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 (ignored.remove(sender)) {
bot.send(sender, "You are no longer ignored.", isPrivate)
event.sendMessage("You are no longer ignored.")
} else {
ignored.add(sender)
bot.send(sender, "You are now ignored.", isPrivate)
event.sendMessage("You are now ignored.")
}
} else {
if (ignored.contains(sender)) {
bot.send(sender, "You are currently ignored.", isPrivate)
event.sendMessage("You are currently ignored.")
} 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()) {
val nicks = args.lowercase().split(" ")
for (nick in nicks) {
@ -138,11 +131,10 @@ class Ignore(bot: Mobibot) : AbstractCommand(bot) {
}
if (ignored.size > 0) {
bot.send(sender, "The following nicks are ignored:", isPrivate)
@Suppress("MagicNumber")
bot.sendList(sender, ignored.sorted(), 8, isPrivate = isPrivate, isIndent = true)
event.sendMessage("The following nicks are ignored:")
event.sendList(ignored.sorted(), 8, isIndent = true)
} 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
import net.thauvin.erik.mobibot.Mobibot
import net.thauvin.erik.mobibot.ReleaseInfo
import net.thauvin.erik.mobibot.Utils.capitalise
import net.thauvin.erik.mobibot.Utils.green
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.commands.links.LinksMgr
import net.thauvin.erik.mobibot.commands.tell.Tell
import org.pircbotx.hooks.types.GenericMessageEvent
import java.lang.management.ManagementFactory
class Info(bot: Mobibot?) : AbstractCommand(bot!!) {
class Info(private val tell: Tell) : AbstractCommand() {
private val allVersions = listOf(
"${ReleaseInfo.PROJECT.capitalise()} ${ReleaseInfo.VERSION} (${green(ReleaseInfo.WEBSITE)})",
"Written by ${ReleaseInfo.AUTHOR} (${green(ReleaseInfo.AUTHOR_URL)})"
)
override val name = "info"
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 isVisible = true
override fun commandResponse(
sender: String,
login: String,
args: String,
isOp: Boolean,
isPrivate: Boolean
) {
with(bot) {
sendList(sender, allVersions, 1, isPrivate = isPrivate)
val info = StringBuilder()
info.append("Uptime: ")
.append(uptime(ManagementFactory.getRuntimeMXBean().uptime))
.append(" [Entries: ")
.append(LinksMgr.entries.size)
if (isOp) {
if (tell.isEnabled()) {
info.append(", Messages: ").append(tell.size())
}
if (twitter.isAutoPost) {
info.append(", Twitter: ").append(twitter.entriesCount())
}
override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
event.sendList(allVersions, 1)
val info = StringBuilder()
info.append("Uptime: ")
.append(uptime(ManagementFactory.getRuntimeMXBean().uptime))
.append(" [Entries: ")
.append(LinksMgr.entries.links.size)
if (isChannelOp(channel, event)) {
if (tell.isEnabled()) {
info.append(", Messages: ").append(tell.size())
}
if (LinksMgr.twitter.isAutoPost) {
info.append(", Twitter: ").append(LinksMgr.twitter.entriesCount())
}
info.append(", Recap: ").append(Recap.recaps.size).append(']')
send(sender, info.toString(), isPrivate)
}
info.append(", Recap: ").append(Recap.recaps.size).append(']')
event.sendMessage(info.toString())
}
}

View file

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

View file

@ -32,35 +32,29 @@
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.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 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 isVisible = true
override fun commandResponse(
sender: String,
login: String,
args: String,
isOp: Boolean,
isPrivate: Boolean
) {
with(bot) {
if (isOp) {
if (modulesNames.isEmpty()) {
send(sender, "There are no enabled modules.", isPrivate)
} else {
send(sender, "The enabled modules are: ", isPrivate)
@Suppress("MagicNumber")
sendList(sender, modulesNames, 7, isPrivate = isPrivate, isIndent = true)
}
override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
if (isChannelOp(channel, event)) {
if (modulesNames.isEmpty()) {
event.respondPrivateMessage("There are no enabled modules.")
} else {
helpDefault(sender, isOp, isPrivate)
event.respondPrivateMessage("The enabled modules are: ")
event.sendList(modulesNames, 7, isIndent = true)
}
} else {
helpResponse(channel, args, event)
}
}
}

View file

@ -32,35 +32,30 @@
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.isChannelOp
import org.pircbotx.hooks.types.GenericMessageEvent
class Msg(bot: Mobibot) : AbstractCommand(bot) {
class Msg : AbstractCommand() {
override val name = "msg"
override val help = listOf(
"To have the bot send a private message to someone:",
helpFormat("%c $name <nick> <text>")
)
override val isOp = true
override val isPublic = true
override val isOpOnly = true
override val isPublic = false
override val isVisible = true
override fun commandResponse(
sender: String,
login: String,
args: String,
isOp: Boolean,
isPrivate: Boolean
) {
if (isOp) {
override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
if (isChannelOp(channel, event)) {
val msg = args.split(" ", limit = 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 {
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
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.isChannelOp
import org.pircbotx.hooks.types.GenericMessageEvent
class Nick(bot: Mobibot) : AbstractCommand(bot) {
class Nick : AbstractCommand() {
override val name = "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 isVisible = true
override fun commandResponse(
sender: String,
login: String,
args: String,
isOp: Boolean,
isPrivate: Boolean
) {
if (isOp) {
bot.changeNick(args)
} else {
bot.helpDefault(sender, isOp, isPrivate)
override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
if (isChannelOp(channel, event)) {
event.bot().sendIRC().changeNick(args)
}
}
}

View file

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

View file

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

View file

@ -31,12 +31,14 @@
*/
package net.thauvin.erik.mobibot.commands
import net.thauvin.erik.mobibot.Mobibot
import net.thauvin.erik.mobibot.ReleaseInfo
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 org.pircbotx.hooks.types.GenericMessageEvent
class Versions(bot: Mobibot) : AbstractCommand(bot) {
class Versions : AbstractCommand() {
private val allVersions = listOf(
"Version: ${ReleaseInfo.VERSION} (${ReleaseInfo.BUILDDATE.toIsoLocalDate()})",
"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 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 isVisible = true
override fun commandResponse(
sender: String,
login: String,
args: String,
isOp: Boolean,
isPrivate: Boolean
) {
if (isOp) {
bot.sendList(sender, allVersions, 1, isPrivate = isPrivate)
} else {
bot.helpDefault(sender, false, isPrivate)
override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
if (isChannelOp(channel, event)) {
event.sendList(allVersions, 1)
}
}
}

View file

@ -33,14 +33,16 @@
package net.thauvin.erik.mobibot.commands.links
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.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.entries.EntriesUtils
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 help = listOf(
"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: ",
helpFormat("${Constants.LINK_CMD}1.1:-")
)
override val isOp = false
override val isOpOnly = false
override val isPublic = true
override val isVisible = true
@ -59,29 +61,22 @@ class Comment(bot: Mobibot) : AbstractCommand(bot) {
const val COMMAND = "comment"
}
override fun commandResponse(
sender: String,
login: String,
args: String,
isOp: Boolean,
isPrivate: Boolean
) {
@Suppress("MagicNumber")
override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
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) {
val entry: EntryLink = LinksMgr.entries[index]
if (entryIndex < LinksMgr.entries.links.size && LinksMgr.isUpToDate(event)) {
val entry: EntryLink = LinksMgr.entries.links[entryIndex]
val commentIndex = cmds[1].toInt() - 1
if (commentIndex < entry.comments.size) {
when (val cmd = cmds[2].trim()) {
"" -> showComment(bot, entry, index, commentIndex) // L1.1:
"-" -> deleteComment(bot, sender, isOp, entry, index, commentIndex) // L11:-
"" -> showComment(entry, entryIndex, commentIndex, event) // L1.1:
"-" -> deleteComment(channel, entry, entryIndex, commentIndex, event) // L1.1:-
else -> {
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>
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(
command: String,
sender: String,
isOp: Boolean,
isPrivate: Boolean
): 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
)
override fun helpResponse(channel: String, topic: String, event: GenericMessageEvent): Boolean {
if (super.helpResponse(channel, topic, event)) {
if (isChannelOp(channel, event)) {
event.sendMessage("To change a comment's author:")
event.sendMessage(helpFormat("${Constants.LINK_CMD}1.1:?<nick>"))
}
return true
}
@ -114,49 +100,52 @@ class Comment(bot: Mobibot) : AbstractCommand(bot) {
}
private fun changeAuthor(
bot: Mobibot,
channel: String,
cmd: String,
sender: String,
isOp: Boolean,
entry: EntryLink,
index: Int,
commentIndex: Int
entryIndex: Int,
commentIndex: Int,
event: GenericMessageEvent
) {
if (isOp && cmd.length > 1) {
if (isChannelOp(channel, event) && cmd.length > 1) {
val comment = entry.getComment(commentIndex)
comment.nick = cmd.substring(1)
bot.send(EntriesUtils.buildComment(index, commentIndex, comment))
LinksMgr.saveEntries(bot, false)
event.sendMessage(EntriesUtils.buildComment(entryIndex, commentIndex, comment))
LinksMgr.entries.save()
} 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(
bot: Mobibot,
sender: String,
isOp: Boolean,
channel: String,
entry: EntryLink,
index: Int,
commentIndex: Int
entryIndex: 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)
bot.send("Comment ${EntriesUtils.buildLinkCmd(index)}.${commentIndex + 1} removed.")
LinksMgr.saveEntries(bot, false)
event.sendMessage("Comment ${EntriesUtils.buildLinkLabel(entryIndex)}.${commentIndex + 1} removed.")
LinksMgr.entries.save()
} 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) {
entry.setComment(commentIndex, cmd, sender)
val comment = entry.getComment(commentIndex)
bot.send(sender, EntriesUtils.buildComment(index, commentIndex, comment), false)
LinksMgr.saveEntries(bot, false)
private fun setComment(
cmd: String,
entry: EntryLink,
entryIndex: Int,
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) {
bot.send(EntriesUtils.buildComment(index, commentIndex, entry.getComment(commentIndex)))
private fun showComment(entry: EntryLink, entryIndex: Int, commentIndex: Int, event: GenericMessageEvent) {
event.sendMessage(EntriesUtils.buildComment(entryIndex, commentIndex, entry.getComment(commentIndex)))
}
}

View file

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

View file

@ -33,15 +33,18 @@
package net.thauvin.erik.mobibot.commands.links
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.bot
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.links.LinksMgr.Companion.entries
import net.thauvin.erik.mobibot.entries.EntriesUtils
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 help = listOf(
"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: ",
helpFormat("%c ${Constants.HELP_CMD} ${Comment.COMMAND}")
)
override val isOp = false
override val isOpOnly = false
override val isPublic = true
override val isVisible = true
override fun commandResponse(
sender: String,
login: String,
args: String,
isOp: Boolean,
isPrivate: Boolean
) {
override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
val cmds = args.substring(1).split(":", limit = 2)
val index = cmds[0].toInt() - 1
val entryIndex = cmds[0].toInt() - 1
if (index < entries.size) {
when (val cmd = cmds[1].trim()) {
"" -> showEntry(index)
"-" -> removeEntry(sender, login, isOp, index) // L1:-
else -> {
if (entryIndex < entries.links.size) {
val cmd = cmds[1].trim()
if (cmd.isBlank()) {
showEntry(entryIndex, event) // L1:
} else if (LinksMgr.isUpToDate(event)) {
if (cmd == "-") {
removeEntry(channel, entryIndex, event) // L1:-
} else {
when (cmd[0]) {
'|' -> changeTitle(cmd, index) // L1:|<title>
'=' -> changeUrl(cmd, login, isOp, index) // L1:=<url>
'?' -> changeAuthor(cmd, sender, isOp, index) // L1:?<author>
else -> addComment(cmd, sender, index) // L1:<comment>
'|' -> changeTitle(cmd, entryIndex, event) // L1:|<title>
'=' -> changeUrl(channel, cmd, entryIndex, event) // L1:=<url>
'?' -> changeAuthor(channel, cmd, entryIndex, event) // L1:?<author>
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())
}
private fun addComment(cmd: String, sender: String, index: Int) {
val entry: EntryLink = entries[index]
val commentIndex = entry.addComment(cmd, sender)
private fun addComment(cmd: String, entryIndex: Int, event: GenericMessageEvent) {
val entry: EntryLink = entries.links[entryIndex]
val commentIndex = entry.addComment(cmd, event.user.nick)
val comment = entry.getComment(commentIndex)
bot.send(sender, EntriesUtils.buildComment(index, commentIndex, comment), false)
LinksMgr.saveEntries(bot, false)
event.sendMessage(EntriesUtils.buildComment(entryIndex, commentIndex, comment))
entries.save()
}
private fun changeTitle(cmd: String, index: Int) {
private fun changeTitle(cmd: String, entryIndex: Int, event: GenericMessageEvent) {
if (cmd.length > 1) {
val entry: EntryLink = entries[index]
val entry: EntryLink = entries.links[entryIndex]
entry.title = cmd.substring(1).trim()
bot.updatePin(entry.link, entry)
bot.send(EntriesUtils.buildLink(index, entry))
LinksMgr.saveEntries(bot, false)
LinksMgr.pinboard.updatePin(event.bot().serverHostname, entry.link, entry)
event.sendMessage(EntriesUtils.buildLink(entryIndex, entry))
entries.save()
}
}
private fun changeUrl(cmd: String, login: String, isOp: Boolean, index: Int) {
val entry: EntryLink = entries[index]
if (entry.login == login || isOp) {
private fun changeUrl(channel: String, cmd: String, entryIndex: Int, event: GenericMessageEvent) {
val entry: EntryLink = entries.links[entryIndex]
if (entry.login == event.user.login || isChannelOp(channel, event)) {
val link = cmd.substring(1)
if (link.matches(LinksMgr.LINK_MATCH.toRegex())) {
val oldLink = entry.link
entry.link = link
bot.updatePin(oldLink, entry)
bot.send(EntriesUtils.buildLink(index, entry))
LinksMgr.saveEntries(bot, false)
LinksMgr.pinboard.updatePin(event.bot().serverHostname, oldLink, entry)
event.sendMessage(EntriesUtils.buildLink(entryIndex, entry))
entries.save()
}
}
}
private fun changeAuthor(cmd: String, sender: String, isOp: Boolean, index: Int) {
if (isOp) {
private fun changeAuthor(channel: String, cmd: String, index: Int, event: GenericMessageEvent) {
if (isChannelOp(channel, event)) {
if (cmd.length > 1) {
val entry: EntryLink = entries[index]
val entry: EntryLink = entries.links[index]
entry.nick = cmd.substring(1)
bot.send(EntriesUtils.buildLink(index, entry))
LinksMgr.saveEntries(bot, false)
LinksMgr.pinboard.updatePin(event.bot().serverHostname, entry.link, entry)
event.sendMessage(EntriesUtils.buildLink(index, entry))
entries.save()
}
} 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) {
val entry: EntryLink = entries[index]
if (entry.login == login || isOp) {
bot.deletePin(index, entry)
entries.removeAt(index)
bot.send("Entry ${EntriesUtils.buildLinkCmd(index)} removed.")
LinksMgr.saveEntries(bot, false)
private fun removeEntry(channel: String, index: Int, event: GenericMessageEvent) {
val entry: EntryLink = entries.links[index]
if (entry.login == event.user.login || isChannelOp(channel, event)) {
LinksMgr.pinboard.deletePin(entry)
LinksMgr.twitter.removeEntry(index)
entries.links.removeAt(index)
event.sendMessage("Entry ${EntriesUtils.buildLinkLabel(index)} removed.")
entries.save()
} 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) {
val entry: EntryLink = entries[index]
bot.send(EntriesUtils.buildLink(index, entry))
private fun showEntry(index: Int, event: GenericMessageEvent) {
val entry: EntryLink = entries.links[index]
event.sendMessage(EntriesUtils.buildLink(index, entry))
if (entry.tags.isNotEmpty()) {
bot.send(EntriesUtils.buildTags(index, entry))
event.sendMessage(EntriesUtils.buildTags(index, entry))
}
if (entry.comments.isNotEmpty()) {
val comments = entry.comments
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
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.isChannelOp
import net.thauvin.erik.mobibot.Utils.sendMessage
import net.thauvin.erik.mobibot.commands.AbstractCommand
import net.thauvin.erik.mobibot.entries.EntriesUtils
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 help = listOf(
"To categorize or tag a URL, use its label and a ${Constants.TAG_CMD}:",
helpFormat("${Constants.LINK_CMD}1${Constants.TAG_CMD}:<+tag|-tag> [...]")
)
override val isOp = false
override val isOpOnly = false
override val isPublic = true
override val isVisible = true
@ -53,33 +56,27 @@ class Tags(bot: Mobibot) : AbstractCommand(bot) {
const val COMMAND = "tags"
}
override fun commandResponse(
sender: String,
login: String,
args: String,
isOp: Boolean,
isPrivate: Boolean
) {
override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
val cmds = args.substring(1).split("${Constants.TAG_CMD}:", limit = 2)
val index = cmds[0].toInt() - 1
if (index < LinksMgr.entries.size) {
if (index < LinksMgr.entries.links.size && LinksMgr.isUpToDate(event)) {
val cmd = cmds[1].trim()
val entry: EntryLink = LinksMgr.entries[index]
val entry: EntryLink = LinksMgr.entries.links[index]
if (cmd.isNotEmpty()) {
if (entry.login == login || isOp) {
if (entry.login == event.user.login || isChannelOp(channel, event)) {
entry.setTags(cmd)
bot.updatePin(entry.link, entry)
bot.send(EntriesUtils.buildTags(index, entry))
LinksMgr.saveEntries(bot, false)
LinksMgr.pinboard.updatePin(event.bot().serverHostname, entry.link, entry)
event.sendMessage(EntriesUtils.buildTags(index, entry))
LinksMgr.entries.save()
} 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 {
if (entry.tags.isNotEmpty()) {
bot.send(EntriesUtils.buildTags(index, entry))
event.sendMessage(EntriesUtils.buildTags(index, entry))
} 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
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.buildCmdSyntax
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.links.LinksMgr.Companion.entries
import net.thauvin.erik.mobibot.entries.EntriesUtils
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) {
@Suppress("MagicNumber")
private val maxEntries = 8
class View : AbstractCommand() {
private val maxEntries = 6
override val name = VIEW_CMD
override val help = listOf(
"To list or search the current URL posts:",
helpFormat("%c $name [<start>] [<query>]")
)
override val isOp = false
override val isOpOnly = false
override val isPublic = true
override val isVisible = true
@ -56,22 +58,16 @@ class View(bot: Mobibot) : AbstractCommand(bot) {
const val VIEW_CMD = "view"
}
override fun commandResponse(
sender: String,
login: String,
args: String,
isOp: Boolean,
isPrivate: Boolean
) {
if (entries.size != 0) {
showPosts(bot, args, sender)
override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
if (entries.links.isNotEmpty()) {
showPosts(args, event)
} 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) {
val max = entries.size
private fun showPosts(args: String, event: GenericMessageEvent) {
val max = entries.links.size
var lcArgs = args.lowercase()
var i = 0
if (lcArgs.isEmpty() && max > maxEntries) {
@ -97,25 +93,32 @@ class View(bot: Mobibot) : AbstractCommand(bot) {
var entry: EntryLink
var sent = 0
while (i < max && sent < maxEntries) {
entry = entries[i]
entry = entries.links[i]
if (lcArgs.isNotBlank()) {
if (entry.matches(lcArgs)) {
bot.send(sender, EntriesUtils.buildLink(i, entry, true), false)
event.sendMessage(EntriesUtils.buildLink(i, entry, true))
sent++
}
} else {
bot.send(sender, EntriesUtils.buildLink(i, entry, true), false)
event.sendMessage(EntriesUtils.buildLink(i, entry, true))
sent++
}
i++
if (sent == maxEntries && i < max) {
bot.send(
sender, "To view more, try: " + bold("${bot.nick}: $name ${i + 1} $lcArgs"), false
event.sendMessage("To view more, try: ")
event.sendMessage(
helpFormat(
buildCmdSyntax(
"%c $name ${i + 1} $lcArgs",
event.bot().nick,
event is PrivateMessageEvent
)
)
)
}
}
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
import net.thauvin.erik.mobibot.Mobibot
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.isChannelOp
import net.thauvin.erik.mobibot.Utils.plural
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.toUtcDateTime
import net.thauvin.erik.mobibot.commands.AbstractCommand
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.
*/
class Tell(bot: Mobibot) : AbstractCommand(bot) {
class Tell(private val serialObject: String) : AbstractCommand() {
// Messages queue
private val messages: MutableList<TellMessage> = mutableListOf()
// Serialized object file
private val serializedObject: String
// Maximum number of days to keep messages
@Suppress("MagicNumber")
private var maxDays = 7
// Message maximum queue size
@Suppress("MagicNumber")
private var maxSize = 50
/**
* Cleans the messages queue.
*/
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())
}
// 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(" ")
if (split.size == 2) {
val id = split[1]
var deleted = false
if (TELL_ALL_KEYWORD.equals(id, ignoreCase = true)) {
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)
deleted = true
}
}
if (deleted) {
save()
bot.send(sender, "Delivered messages have been deleted.", isPrivate)
event.sendMessage("Delivered messages have been deleted.")
} else {
bot.send(sender, "No delivered messages were found.", isPrivate)
event.sendMessage("No delivered messages were found.")
}
} else {
var found = false
for (message in messages) {
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)
save()
bot.send(sender, "Your message was deleted from the queue.", isPrivate)
event.sendMessage("Your message was deleted from the queue.")
deleted = true
break
}
}
if (!deleted) {
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 {
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 {
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}"),
"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 isVisible: Boolean = isEnabled()
override fun commandResponse(
sender: String,
login: String,
args: String,
isOp: Boolean,
isPrivate: Boolean
) {
override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
if (isEnabled()) {
if (args.isBlank()) {
helpResponse(args, sender, isOp, isPrivate)
helpResponse(channel, args, event)
} else if (args.startsWith(View.VIEW_CMD)) {
if (bot.isOp(sender) && "${View.VIEW_CMD} $TELL_ALL_KEYWORD" == args) {
viewAll(sender, isPrivate)
if (isChannelOp(channel, event) && "${View.VIEW_CMD} $TELL_ALL_KEYWORD" == args) {
viewAll(event)
} else {
viewMessages(sender, isPrivate)
viewMessages(event)
}
} else if (args.startsWith("$TELL_DEL_KEYWORD ")) {
deleteMessage(sender, args, isOp, isPrivate)
deleteMessage(channel, args, event)
} else {
newMessage(sender, args, isOp, isPrivate)
newMessage(channel, args, event)
}
if (clean()) {
save()
@ -169,21 +167,19 @@ class Tell(bot: Mobibot) : AbstractCommand(bot) {
}
// 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)
if (split.size == 2 && split[1].isNotBlank() && split[1].contains(" ")) {
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)
save()
bot.send(
sender, "Message [ID ${message.id}] was queued for ${bold(message.recipient)}", isPrivate
)
event.sendMessage("Message [ID ${message.id}] was queued for ${bold(message.recipient)}")
} else {
bot.send(sender, "Sorry, the messages queue is currently full.", isPrivate)
event.sendMessage("Sorry, the messages queue is currently full.")
}
} else {
helpResponse(args, sender, isOp, isPrivate)
helpResponse(channel, args, event)
}
}
@ -191,34 +187,30 @@ class Tell(bot: Mobibot) : AbstractCommand(bot) {
* Saves the messages queue.
*/
private fun save() {
TellMessagesMgr.save(serializedObject, messages, bot.logger)
TellMessagesMgr.save(serialObject, messages)
}
/**
* Checks and sends messages.
*/
@JvmOverloads
fun send(nickname: String, isMessage: Boolean = false) {
if (isEnabled() && nickname != bot.nick) {
fun send(event: GenericUserEvent) {
val nickname = event.user.nick
if (isEnabled() && nickname != event.getBot<PircBotX>().nick) {
messages.stream().filter { message: TellMessage -> message.isMatch(nickname) }
.forEach { message: TellMessage ->
if (message.recipient.equals(nickname, ignoreCase = true) && !message.isReceived) {
if (message.sender == nickname) {
if (!isMessage) {
bot.send(
nickname,
"${bold("You")} wanted me to remind you: ${reverseColor(message.message)}",
true
if (event !is MessageEvent) {
event.user.send().message(
"${bold("You")} wanted me to remind you: ${reverseColor(message.message)}"
)
message.isReceived = true
message.isNotified = true
save()
}
} else {
bot.send(
nickname,
"${message.sender} wanted me to tell you: ${reverseColor(message.message)}",
true
event.user.send().message(
"${message.sender} wanted me to tell you: ${reverseColor(message.message)}"
)
message.isReceived = true
save()
@ -226,11 +218,9 @@ class Tell(bot: Mobibot) : AbstractCommand(bot) {
} else if (message.sender.equals(nickname, ignoreCase = true) && message.isReceived
&& !message.isNotified
) {
bot.send(
nickname,
"Your message ${reverseColor("[ID " + message.id + ']')} was sent to "
+ "${bold(message.recipient)} on ${message.receptionDate.toUtcDateTime()}",
true
event.user.send().message(
"Your message ${reverseColor("[ID ${message.id}]")} was sent to "
+ "${bold(message.recipient)} on ${message.receptionDate}"
)
message.isNotified = true
save()
@ -247,66 +237,55 @@ class Tell(bot: Mobibot) : AbstractCommand(bot) {
fun size(): Int = messages.size
// View all messages.
private fun viewAll(sender: String, isPrivate: Boolean) {
private fun viewAll(event: GenericMessageEvent) {
if (messages.isNotEmpty()) {
for (message in messages) {
bot.send(
sender, bold(message.sender) + ARROW + bold(message.recipient)
+ " [ID: " + message.id + ", "
+ (if (message.isReceived) "DELIVERED" else "QUEUED") + ']',
isPrivate
event.sendMessage(
"${bold(message.sender)}$ARROW${bold(message.recipient)} [ID: ${message.id}, " +
(if (message.isReceived) "DELIVERED]" else "QUEUED]")
)
}
} else {
bot.send(sender, "There are no messages in the queue.", isPrivate)
event.sendMessage("There are no messages in the queue.")
}
}
// View messages.
private fun viewMessages(sender: String, isPrivate: Boolean) {
private fun viewMessages(event: GenericMessageEvent) {
var hasMessage = false
for (message in messages) {
if (message.isMatch(sender)) {
if (message.isMatch(event.user.nick)) {
if (!hasMessage) {
hasMessage = true
bot.send(sender, "Here are your messages: ", isPrivate)
hasMessage = true; event.sendMessage("Here are your messages: ")
}
if (message.isReceived) {
bot.send(
sender,
bold(message.sender) + ARROW + bold(message.recipient)
+ " [${message.receptionDate.toUtcDateTime()}, ID: "
+ bold(message.id) + ", DELIVERED]",
isPrivate
event.sendMessage(
bold(message.sender) + ARROW + bold(message.recipient) +
" [${message.receptionDate.toUtcDateTime()}, ID: ${bold(message.id)}, DELIVERED]"
)
} else {
bot.send(
sender,
bold(message.sender) + ARROW + bold(message.recipient)
+ " [${message.queued.toUtcDateTime()}, ID: "
+ bold(message.id) + ", QUEUED]",
isPrivate
event.sendMessage(
bold(message.sender) + ARROW + bold(message.recipient) +
" [${message.queued.toUtcDateTime()}, ID: ${bold(message.id)}, QUEUED]"
)
}
bot.send(sender, helpFormat(message.message), isPrivate)
event.sendMessage(helpFormat(message.message))
}
}
if (!hasMessage) {
bot.send(sender, "You have no messages in the queue.", isPrivate)
event.sendMessage("You have no messages in the queue.")
} else {
bot.send(sender, "To delete one or all delivered messages:", isPrivate)
bot.send(
sender,
event.sendMessage("To delete one or all delivered messages:")
event.sendMessage(
helpFormat(
buildCmdSyntax(
"%c $name $TELL_DEL_KEYWORD <id|$TELL_ALL_KEYWORD>",
bot.nick,
isPrivate
event.user.nick,
true
)
),
isPrivate
)
)
bot.send(sender, help.last(), isPrivate)
event.sendMessage(help.last())
}
}
@ -324,9 +303,6 @@ class Tell(bot: Mobibot) : AbstractCommand(bot) {
// Arrow
private const val ARROW = " --> "
// Serialized object file extension
private const val SER_EXT = ".ser"
// All keyword
private const val TELL_ALL_KEYWORD = "all"
@ -341,8 +317,7 @@ class Tell(bot: Mobibot) : AbstractCommand(bot) {
initProperties(MAX_DAYS_PROP, MAX_SIZE_PROP)
// Load the message queue
serializedObject = bot.logsDir + bot.name + SER_EXT
messages.addAll(TellMessagesMgr.load(serializedObject, bot.logger))
messages.addAll(TellMessagesMgr.load(serialObject))
if (clean()) {
save()
}

View file

@ -66,12 +66,12 @@ class TellMessage internal constructor(
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
/**
* Returns {@code true) if the message was received.
* Returns {@code true} if the message was received.
*/
var isReceived = false
set(value) {

View file

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

View file

@ -1,5 +1,5 @@
/*
* Kill.kt
* Entries.kt
*
* Copyright (c) 2004-2021, Erik C. Thauvin (erik@thauvin.net)
* All rights reserved.
@ -30,26 +30,26 @@
* 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) {
override val name = "kill"
override val help = emptyList<String>()
override val isOp = true
override val isPublic = false
override val isVisible = false
class Entries(
var channel: String = "",
var ircServer: String = "",
var logsDir: String = "",
var backlogs: String = ""
) {
val links = mutableListOf<EntryLink>()
override fun commandResponse(
sender: String,
login: String,
args: String,
isOp: Boolean,
isPrivate: Boolean
) {
if (isOp) {
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 {
/**
* 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.
*/
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.
*/
@JvmOverloads
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(']')
if (isView && entry.comments.isNotEmpty()) {
buff.append("[+").append(entry.comments.size).append(']')
@ -76,5 +76,5 @@ object EntriesUtils {
* Build an entry's tags/categories for display on the channel.
*/
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 {
comments.add(EntryComment(comment, nick))
return comments.size - 1
return comments.lastIndex
}
/**
* Deletes a specific comment.
*/
fun deleteComment(index: Int) {
fun deleteComment(index: Int): Boolean {
if (index < comments.size) {
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.
*/
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)
}
}

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
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.sendMessage
import org.pircbotx.hooks.events.PrivateMessageEvent
import org.pircbotx.hooks.types.GenericMessageEvent
/**
* The `Module` abstract class.
*/
abstract class AbstractModule(val bot: Mobibot) {
abstract class AbstractModule {
/**
* The module's commands, if any.
*/
@ -51,12 +54,7 @@ abstract class AbstractModule(val bot: Mobibot) {
/**
* Responds to a command.
*/
abstract fun commandResponse(
sender: String,
cmd: String,
args: String,
isPrivate: Boolean
)
abstract fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent)
/**
* Returns the module's property keys.
@ -74,9 +72,9 @@ abstract class AbstractModule(val bot: Mobibot) {
/**
* Responds with the module's help.
*/
open fun helpResponse(sender: String, isPrivate: Boolean): Boolean {
open fun helpResponse(event: GenericMessageEvent): Boolean {
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
}

View file

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

View file

@ -34,40 +34,44 @@ package net.thauvin.erik.mobibot.modules
import net.thauvin.erik.crypto.CryptoException
import net.thauvin.erik.crypto.CryptoPrice
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.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.
*/
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).
*/
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)"
with(bot) {
if (args.matches("\\w+( [a-zA-Z]{3}+)?".toRegex())) {
try {
val price = currentPrice(args.split(' '))
val amount = try {
price.toCurrency()
} catch (ignore: IllegalArgumentException) {
price.amount
}
send(sender, PublicMessage("${price.base}: $amount [${price.currency}]"))
} catch (e: CryptoException) {
if (logger.isWarnEnabled) logger.warn("$debugMessage => ${e.statusCode}", e)
send(e.message)
} catch (e: Exception) {
if (logger.isErrorEnabled) logger.error(debugMessage, e)
send("An error has occurred while retrieving the cryptocurrency market price.")
if (args.matches("\\w+( [a-zA-Z]{3}+)?".toRegex())) {
try {
val price = currentPrice(args.split(' '))
val amount = try {
price.toCurrency()
} catch (ignore: IllegalArgumentException) {
price.amount
}
} else {
helpResponse(sender, isPrivate)
event.respond("${price.base} current price is $amount [${price.currency}]")
} catch (e: CryptoException) {
if (logger.isWarnEnabled) logger.warn("$debugMessage => ${e.statusCode}", e)
event.sendMessage(e.message!!)
} catch (e: IOException) {
if (logger.isErrorEnabled) logger.error(debugMessage, e)
event.sendMessage("An IO error has occurred while retrieving the cryptocurrency market price.")
}
} else {
helpResponse(event)
}
}
companion object {

View file

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

View file

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

View file

@ -31,45 +31,49 @@
*/
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.encodeUrl
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.urlReader
import net.thauvin.erik.mobibot.msg.ErrorMessage
import net.thauvin.erik.mobibot.msg.Message
import net.thauvin.erik.mobibot.msg.NoticeMessage
import org.jibble.pircbot.Colors
import org.json.JSONException
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.net.URL
/**
* The GoogleSearch module.
*/
class GoogleSearch(bot: Mobibot) : ThreadedModule(bot) {
class GoogleSearch : ThreadedModule() {
private val logger: Logger = LoggerFactory.getLogger(GoogleSearch::class.java)
/**
* Searches Google.
*/
override fun run(sender: String, cmd: String, args: String, isPrivate: Boolean) {
with(bot) {
if (args.isNotBlank()) {
try {
val results = searchGoogle(
args, properties[GOOGLE_API_KEY_PROP],
properties[GOOGLE_CSE_KEY_PROP]
)
for (msg in results) {
send(sender, msg)
}
} catch (e: ModuleException) {
if (logger.isWarnEnabled) logger.warn(e.debugMessage, e)
send(sender, e.message, isPrivate)
override fun run(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
if (args.isNotBlank()) {
try {
val results = searchGoogle(
args, properties[GOOGLE_API_KEY_PROP],
properties[GOOGLE_CSE_KEY_PROP]
)
for (msg in results) {
event.sendMessage(channel, msg)
}
} else {
helpResponse(sender, isPrivate)
} catch (e: ModuleException) {
if (logger.isWarnEnabled) logger.warn(e.debugMessage, e)
event.sendMessage(e.message!!)
}
} else {
helpResponse(event)
}
}
@ -92,29 +96,33 @@ class GoogleSearch(bot: Mobibot) : ThreadedModule(bot) {
if (apiKey.isNullOrBlank() || cseKey.isNullOrBlank()) {
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 {
val url = URL(
"https://www.googleapis.com/customsearch/v1?key=$apiKey&cx=$cseKey" +
"&q=${encodeUrl(query)}&filter=1&num=5&alt=json"
)
val json = JSONObject(urlReader(url))
val ja = json.getJSONArray("items")
for (i in 0 until ja.length()) {
val j = ja.getJSONObject(i)
results.add(NoticeMessage(unescapeXml(j.getString("title"))))
results.add(NoticeMessage(helpFormat(j.getString("link"), false), Colors.DARK_GREEN))
if (json.has("items")) {
val ja = json.getJSONArray("items")
for (i in 0 until ja.length()) {
val j = ja.getJSONObject(i)
results.add(NoticeMessage(unescapeXml(j.getString("title"))))
results.add(NoticeMessage(helpFormat(j.getString("link"), false), Colors.DARK_GREEN))
}
} else {
results.add(ErrorMessage("No results found.", Colors.RED))
}
} catch (e: IOException) {
throw ModuleException("searchGoogle($query)", "An IO error has occurred searching Google.", e)
} catch (e: JSONException) {
throw ModuleException("searchGoogle($query)", "A JSON error has occurred searching Google.", e)
}
results
} 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.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.helpFormat
import net.thauvin.erik.mobibot.Utils.sendMessage
import net.thauvin.erik.mobibot.Utils.urlReader
import net.thauvin.erik.mobibot.msg.Message
import net.thauvin.erik.mobibot.msg.PublicMessage
import org.json.JSONException
import org.json.JSONObject
import org.pircbotx.hooks.types.GenericMessageEvent
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.io.IOException
import java.net.URL
/**
* The Joke module.
*/
class Joke(bot: Mobibot) : ThreadedModule(bot) {
override fun commandResponse(
sender: String,
cmd: String,
args: String,
isPrivate: Boolean
) {
class Joke : ThreadedModule() {
private val logger: Logger = LoggerFactory.getLogger(Joke::class.java)
override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
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/).
*/
override fun run(sender: String, cmd: String, args: String, isPrivate: Boolean) {
with(bot) {
override fun run(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
with(event.bot()) {
try {
send(cyan(randomJoke().msg))
sendIRC().notice(channel, cyan(randomJoke().msg))
} catch (e: ModuleException) {
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
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 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.net.InetAddress
import java.net.UnknownHostException
@ -42,52 +45,51 @@ import java.net.UnknownHostException
/**
* The Lookup module.
*/
class Lookup(bot: Mobibot) : AbstractModule(bot) {
override fun commandResponse(
sender: String,
cmd: String,
args: String,
isPrivate: Boolean
) {
class Lookup : AbstractModule() {
private val logger: Logger = LoggerFactory.getLogger(Lookup::class.java)
override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
if (args.matches("(\\S.)+(\\S)+".toRegex())) {
with(bot) {
try {
nslookup(args).split(',').forEach {
send(it.trim())
}
} catch (ignore: UnknownHostException) {
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]?)").toRegex()
)
) {
try {
val lines = whois(args)
if (lines.isNotEmpty()) {
var line: String
for (rawLine in lines) {
line = rawLine.trim()
if (line.isNotEmpty() && line[0] != '#') {
send(line)
try {
event.respondWith(nslookup(args).prependIndent())
} catch (ignore: UnknownHostException) {
if (args.matches(
("(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(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]?)").toRegex()
)
) {
try {
val lines = whois(args)
if (lines.isNotEmpty()) {
var line: String
var hasData = false
for (rawLine in lines) {
line = rawLine.trim()
if (line.matches("^\\b(?!\\b[Cc]omment\\b)\\w+\\b: .*$".toRegex())) {
if (!hasData) {
event.respondWith(line)
hasData = true
} else {
event.bot().sendIRC().notice(event.user.nick, line)
}
}
} else {
send("Unknown host.")
}
} catch (ioe: IOException) {
if (logger.isDebugEnabled) {
logger.debug("Unable to perform whois IP lookup: $args", ioe)
}
send("Unable to perform whois IP lookup: ${ioe.message}")
} else {
event.respond("Unknown host.")
}
} else {
send("Unknown host.")
} catch (ioe: IOException) {
if (logger.isWarnEnabled) {
logger.warn("Unable to perform whois IP lookup: $args", ioe)
}
event.respond("Unable to perform whois IP lookup: ${ioe.message}")
}
} else {
event.respond("Unknown host.")
}
}
} else {
helpResponse(sender, true)
helpResponse(event)
}
}

View file

@ -31,24 +31,20 @@
*/
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 org.pircbotx.hooks.types.GenericMessageEvent
import kotlin.random.Random
/**
* The Ping module.
*/
class Ping(bot: Mobibot) : AbstractModule(bot) {
class Ping : AbstractModule() {
/**
* {@inheritDoc}
*/
override fun commandResponse(
sender: String,
cmd: String,
args: String,
isPrivate: Boolean
) {
bot.action(randomPing())
override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
event.bot().sendIRC().action(channel, randomPing())
}
companion object {

View file

@ -32,16 +32,17 @@
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.helpFormat
import org.pircbotx.hooks.types.GenericMessageEvent
import kotlin.random.Random
/**
* Simple module example in Kotlin.
*/
class RockPaperScissors(bot: Mobibot) : AbstractModule(bot) {
class RockPaperScissors : AbstractModule() {
init {
with(commands) {
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 botHand = Hands.values()[Random.nextInt(0, Hands.values().size)]
with(bot) {
send("${hand.emoji} vs. ${botHand.emoji}")
with(event.bot()) {
sendIRC().message(channel, "${hand.emoji} vs. ${botHand.emoji}")
when {
hand == botHand -> {
action("tied.")
sendIRC().action(channel, "tied.")
}
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 -> {
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
import net.thauvin.erik.mobibot.Mobibot
import net.thauvin.erik.mobibot.Utils.capitalise
import net.thauvin.erik.mobibot.Utils.encodeUrl
import net.thauvin.erik.mobibot.Utils.helpFormat
import net.thauvin.erik.mobibot.Utils.sendMessage
import net.thauvin.erik.mobibot.Utils.unescapeXml
import net.thauvin.erik.mobibot.Utils.urlReader
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 org.json.JSONException
import org.json.JSONObject
import org.pircbotx.hooks.types.GenericMessageEvent
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.io.IOException
import java.net.URL
/**
* 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.
*/
override fun run(sender: String, cmd: String, args: String, isPrivate: Boolean) {
with(bot) {
if (args.isNotBlank()) {
try {
val messages = getQuote(args, properties[ALPHAVANTAGE_API_KEY_PROP])
for (msg in messages) {
send(sender, msg)
}
} catch (e: ModuleException) {
if (logger.isWarnEnabled) logger.warn(e.debugMessage, e)
send(e.message)
override fun run(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
if (args.isNotBlank()) {
try {
val messages = getQuote(args, properties[ALPHAVANTAGE_API_KEY_PROP])
for (msg in messages) {
event.sendMessage(channel, msg)
}
} else {
helpResponse(sender, isPrivate)
} catch (e: ModuleException) {
if (logger.isWarnEnabled) logger.warn(e.debugMessage, e)
event.sendMessage(e.message!!)
}
} else {
helpResponse(event)
}
}
@ -129,9 +132,9 @@ class StockQuote(bot: Mobibot) : ThreadedModule(bot) {
"${STOCK_CMD.capitalise()} is disabled. The API key is missing."
)
}
return if (symbol.isNotBlank()) {
val messages = mutableListOf<Message>()
if (symbol.isNotBlank()) {
val debugMessage = "getQuote($symbol)"
val messages = mutableListOf<Message>()
var response: String
try {
with(messages) {
@ -161,56 +164,55 @@ class StockQuote(bot: Mobibot) : ThreadedModule(bot) {
val quote = json.getJSONObject("Global Quote")
if (quote.isEmpty) {
add(ErrorMessage(INVALID_SYMBOL))
return messages
}
} else {
add(
PublicMessage(
"Symbol: " + unescapeXml(quote.getString("01. symbol"))
+ " [" + unescapeXml(symbolInfo.getString("2. name")) + ']'
add(
PublicMessage(
"Symbol: " + unescapeXml(quote.getString("01. symbol"))
+ " [" + unescapeXml(symbolInfo.getString("2. name")) + ']'
)
)
)
@Suppress("MagicNumber")
val pad = 10
val pad = 10
add(
PublicMessage(
"Price:".padEnd(pad).prependIndent()
+ unescapeXml(quote.getString("05. price"))
add(
PublicMessage(
"Price:".padEnd(pad).prependIndent()
+ unescapeXml(quote.getString("05. price"))
)
)
)
add(
PublicMessage(
"Previous:".padEnd(pad).prependIndent()
+ unescapeXml(quote.getString("08. previous close"))
add(
PublicMessage(
"Previous:".padEnd(pad).prependIndent()
+ unescapeXml(quote.getString("08. previous close"))
)
)
)
val data = arrayOf(
"Open" to "02. open",
"High" to "03. high",
"Low" to "04. low",
"Volume" to "06. volume",
"Latest" to "07. latest trading day"
)
val data = arrayOf(
"Open" to "02. open",
"High" to "03. high",
"Low" to "04. low",
"Volume" to "06. volume",
"Latest" to "07. latest trading day"
)
data.forEach {
add(
NoticeMessage(
"${it.first}:".padEnd(pad).prependIndent()
+ unescapeXml(quote.getString(it.second))
)
)
}
data.forEach {
add(
NoticeMessage(
"${it.first}:".padEnd(pad).prependIndent()
+ unescapeXml(quote.getString(it.second))
"Change:".padEnd(pad).prependIndent()
+ unescapeXml(quote.getString("09. change"))
+ " [" + unescapeXml(quote.getString("10. change percent")) + ']'
)
)
}
add(
NoticeMessage(
"Change:".padEnd(pad).prependIndent()
+ unescapeXml(quote.getString("09. change"))
+ " [" + unescapeXml(quote.getString("10. change percent")) + ']'
)
)
}
}
} catch (e: IOException) {
@ -218,10 +220,10 @@ class StockQuote(bot: Mobibot) : ThreadedModule(bot) {
} catch (e: NullPointerException) {
throw ModuleException(debugMessage, "An error has occurred retrieving a stock quote.", e)
}
messages
} 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.runBlocking
import net.thauvin.erik.mobibot.Mobibot
import org.pircbotx.hooks.types.GenericMessageEvent
/**
* The `ThreadedModule` class.
*/
abstract class ThreadedModule(bot: Mobibot) : AbstractModule(bot) {
override fun commandResponse(
sender: String,
cmd: String,
args: String,
isPrivate: Boolean
) {
if (isEnabled && args.isNotEmpty()) {
abstract class ThreadedModule : AbstractModule() {
override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
if (isEnabled && event.message.isNotEmpty()) {
runBlocking {
launch {
run(sender, cmd, args, isPrivate)
run(channel, cmd, args, event)
}
}
} else {
helpResponse(sender, isPrivate)
helpResponse(event)
}
}
/**
* Runs the thread.
*/
abstract fun run(
sender: String,
cmd: String,
args: String,
isPrivate: Boolean
)
abstract fun run(channel: String, cmd: String, args: String, event: GenericMessageEvent)
}

View file

@ -34,21 +34,26 @@ package net.thauvin.erik.mobibot.modules
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import net.thauvin.erik.mobibot.Constants
import net.thauvin.erik.mobibot.Mobibot
import net.thauvin.erik.mobibot.TwitterTimer
import net.thauvin.erik.mobibot.Utils.helpFormat
import net.thauvin.erik.mobibot.commands.links.LinksMgr
import net.thauvin.erik.mobibot.entries.EntriesUtils
import net.thauvin.erik.mobibot.msg.Message
import net.thauvin.erik.mobibot.msg.NoticeMessage
import org.pircbotx.hooks.types.GenericMessageEvent
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import twitter4j.TwitterException
import twitter4j.TwitterFactory
import twitter4j.conf.ConfigurationBuilder
import java.util.Timer
/**
* 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.
private val entries: MutableSet<Int> = HashSet()
@ -87,16 +92,14 @@ class Twitter(bot: Mobibot) : ThreadedModule(bot) {
* Send a notification to the registered Twitter handle.
*/
fun notification(msg: String) {
with(bot) {
if (isEnabled && !handle.isNullOrBlank()) {
runBlocking {
launch {
try {
post(message = msg, isDm = true)
if (logger.isDebugEnabled) logger.debug("Notified @$handle: $msg")
} catch (e: ModuleException) {
if (logger.isWarnEnabled) logger.warn("Failed to notify @$handle: $msg", e)
}
if (isEnabled && !handle.isNullOrBlank()) {
runBlocking {
launch {
try {
post(message = msg, isDm = true)
if (logger.isDebugEnabled) logger.debug("Notified @$handle: $msg")
} catch (e: ModuleException) {
if (logger.isWarnEnabled) logger.warn("Failed to notify @$handle: $msg", e)
}
}
}
@ -107,7 +110,7 @@ class Twitter(bot: Mobibot) : ThreadedModule(bot) {
* Posts on Twitter.
*/
@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(
properties[CONSUMER_KEY_PROP],
properties[CONSUMER_SECRET_PROP],
@ -123,35 +126,32 @@ class Twitter(bot: Mobibot) : ThreadedModule(bot) {
* Post an entry to twitter.
*/
fun postEntry(index: Int) {
with(bot) {
if (isAutoPost && hasEntry(index) && LinksMgr.entries.size >= index) {
val entry = LinksMgr.entries[index]
val msg = "${entry.title} ${entry.link} via ${entry.nick} on $channel"
runBlocking {
launch {
try {
if (logger.isDebugEnabled) {
logger.debug("Posting {} to Twitter.", EntriesUtils.buildLinkCmd(index))
}
post(message = msg, isDm = false)
} catch (e: ModuleException) {
if (logger.isWarnEnabled) logger.warn("Failed to post entry on Twitter.", e)
if (isAutoPost && hasEntry(index) && LinksMgr.entries.links.size >= index) {
val entry = LinksMgr.entries.links[index]
val msg = "${entry.title} ${entry.link} via ${entry.nick} on ${entry.channel}"
runBlocking {
launch {
try {
if (logger.isDebugEnabled) {
logger.debug("Posting {} to Twitter.", EntriesUtils.buildLinkLabel(index))
}
post(message = msg, isDm = false)
} catch (e: ModuleException) {
if (logger.isWarnEnabled) logger.warn("Failed to post entry on Twitter.", e)
}
}
removeEntry(index)
}
removeEntry(index)
}
}
fun queueEntry(index: Int) {
if (isAutoPost) {
addEntry(index)
if (bot.logger.isDebugEnabled) {
bot.logger.debug("Scheduling {} for posting on Twitter.", EntriesUtils.buildLinkCmd(index))
if (logger.isDebugEnabled) {
logger.debug("Scheduling {} for posting on Twitter.", EntriesUtils.buildLinkLabel(index))
}
@Suppress("MagicNumber")
bot.timer.schedule(TwitterTimer(bot, index), Constants.TIMER_DELAY * 60L * 1000L)
timer.schedule(TwitterTimer(this, index), Constants.TIMER_DELAY * 60L * 1000L)
}
}
@ -162,18 +162,12 @@ class Twitter(bot: Mobibot) : ThreadedModule(bot) {
/**
* Posts to twitter.
*/
override fun run(sender: String, cmd: String, args: String, isPrivate: Boolean) {
with(bot) {
try {
send(
sender,
post(sender, "$args (by $sender on $channel)", false).msg,
isPrivate
)
} catch (e: ModuleException) {
if (logger.isWarnEnabled) logger.warn(e.debugMessage, e)
send(sender, e.message, isPrivate)
}
override fun run(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
try {
event.respond(post(event.user.nick, "$args (by ${event.user.nick} on $channel)", false))
} catch (e: ModuleException) {
if (logger.isWarnEnabled) logger.warn(e.debugMessage, e)
event.respond(e.message)
}
}
@ -181,6 +175,7 @@ class Twitter(bot: Mobibot) : ThreadedModule(bot) {
* Post all the entries to Twitter on shutdown.
*/
fun shutdown() {
timer.cancel()
if (isAutoPost) {
for (index in entries) {
postEntry(index)
@ -213,23 +208,23 @@ class Twitter(bot: Mobibot) : ThreadedModule(bot) {
handle: String?,
message: String,
isDm: Boolean
): Message {
): String {
return try {
val cb = ConfigurationBuilder()
cb.setDebugEnabled(true)
.setOAuthConsumerKey(consumerKey)
.setOAuthConsumerSecret(consumerSecret)
.setOAuthAccessToken(token).setOAuthAccessTokenSecret(tokenSecret)
val cb = ConfigurationBuilder().apply {
setDebugEnabled(true)
setOAuthConsumerKey(consumerKey)
setOAuthConsumerSecret(consumerSecret)
setOAuthAccessToken(token)
setOAuthAccessTokenSecret(tokenSecret)
}
val tf = TwitterFactory(cb.build())
val twitter = tf.instance
if (!isDm) {
val status = twitter.updateStatus(message)
NoticeMessage(
"You message was posted to https://twitter.com/${twitter.screenName}/statuses/${status.id}"
)
"Your message was posted to https://twitter.com/${twitter.screenName}/statuses/${status.id}"
} else {
val dm = twitter.sendDirectMessage(handle, message)
NoticeMessage(dm.text)
dm.text
}
} catch (e: TwitterException) {
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.Country
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.capitalise
import net.thauvin.erik.mobibot.Utils.capitalizeWords
import net.thauvin.erik.mobibot.Utils.encodeUrl
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.Message
import net.thauvin.erik.mobibot.msg.NoticeMessage
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
/**
* 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.
*/
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()) {
with(bot) {
try {
val messages = getWeather(args, properties[OWM_API_KEY_PROP])
if (messages[0].isError) {
helpResponse(sender, isPrivate)
} else {
for (msg in messages) {
send(sender, msg)
}
try {
val messages = getWeather(args, properties[OWM_API_KEY_PROP])
if (messages[0].isError) {
helpResponse(event)
} else {
for (msg in messages) {
event.sendMessage(channel, msg)
}
} catch (e: ModuleException) {
if (logger.isDebugEnabled) logger.debug(e.debugMessage, e)
send(e.message)
}
} catch (e: ModuleException) {
if (logger.isWarnEnabled) logger.warn(e.debugMessage, e)
event.respond(e.message)
}
} else {
helpResponse(sender, isPrivate)
helpResponse(event)
}
}
@ -90,7 +93,6 @@ class Weather2(bot: Mobibot) : ThreadedModule(bot) {
* Converts and rounds temperature from °F to °C.
*/
fun ftoC(d: Double?): Pair<Int, Int> {
@Suppress("MagicNumber")
val c = (d!! - 32) * 5 / 9
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.
*/
fun mphToKmh(w: Double): Pair<Int, Int> {
@Suppress("MagicNumber")
val kmh = w * 1.60934
return w.roundToInt() to kmh.roundToInt()
}

View file

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

View file

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

View file

@ -31,12 +31,14 @@
*/
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 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 java.io.FileNotFoundException
import java.net.MalformedURLException
import java.net.UnknownHostException
@ -48,26 +50,24 @@ class FeedReaderTest {
@Test
fun readFeedTest() {
var messages = readFeed("https://feeds.thauvin.net/ethauvin")
assertThat(messages.size).describedAs("messages = 10").isEqualTo(10)
assertThat(messages[1].msg).describedAs("feed entry url").contains("ethauvin")
assertThat(messages.size, "size = 10").isEqualTo(10)
assertThat(messages[1].msg, "feed entry url").contains("ethauvin")
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)
assertThat(messages.size).describedAs("messages = 84").isEqualTo(84)
assertThat(messages.last().msg).describedAs("example entry url").contains("http://example.com/test/")
assertThat(messages.size, "messages = 84").isEqualTo(84)
assertThat(messages.last().msg, "example entry url").contains("http://example.com/test/")
assertThatThrownBy { readFeed("blah") }.describedAs("invalid URL")
.isInstanceOf(MalformedURLException::class.java)
assertThat { readFeed("blah") }.isFailure().isInstanceOf(MalformedURLException::class.java)
assertThatThrownBy { readFeed("https://www.example.com") }.describedAs("not a feed")
.isInstanceOf(FeedException::class.java)
assertThat { readFeed("https://www.example.com") }.isFailure().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)
assertThatThrownBy { readFeed("https://www.doesnotexists.com") }.describedAs("unknown host")
assertThat { readFeed("https://www.doesnotexists.com") }.isFailure()
.isInstanceOf(UnknownHostException::class.java)
}
}

View file

@ -1,5 +1,5 @@
/*
* PinboardUtilsTest.kt
* PinboardTest.kt
*
* Copyright (c) 2004-2021, Erik C. Thauvin (erik@thauvin.net)
* All rights reserved.
@ -32,45 +32,39 @@
package net.thauvin.erik.mobibot
import net.thauvin.erik.mobibot.PinboardUtils.toTimestamp
import net.thauvin.erik.mobibot.entries.EntryLink
import net.thauvin.erik.pinboard.PinboardPoster
import org.testng.Assert.assertFalse
import org.testng.Assert.assertTrue
import org.testng.annotations.Test
import java.net.URL
import java.util.Date
class PinboardUtilsTest : LocalProperties() {
class PinboardTest : LocalProperties() {
private val pinboard = Pinboard()
@Test
fun pinboardTest() {
fun testPinboard() {
val apiToken = getProperty("pinboard-api-token")
val pinboard = PinboardPoster(apiToken)
val url = "https://www.example.com/"
val ircServer = "irc.test.com"
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")
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")
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")
PinboardUtils.deletePin(pinboard, entry)
pinboard.deletePin(entry)
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 {
val response = Utils.urlReader(
URL(

View file

@ -31,6 +31,8 @@
*/
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.bold
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.urlReader
import net.thauvin.erik.mobibot.msg.Message.Companion.DEFAULT_COLOR
import org.assertj.core.api.Assertions.assertThat
import org.jibble.pircbot.Colors
import org.pircbotx.Colors
import org.testng.annotations.BeforeClass
import org.testng.annotations.Test
import java.io.File
@ -86,59 +87,59 @@ class UtilsTest {
val dir = "dir"
val sep = '/'
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)
assertThat(url.appendIfMissing(sep)).describedAs("appendIfMissing(url)").isEqualTo("$url$sep")
assertThat("$url$sep".appendIfMissing(sep)).describedAs("appendIfMissing($url$sep)").isEqualTo("$url$sep")
assertThat(url.appendIfMissing(sep), "appendIfMissing(url)").isEqualTo("$url$sep")
assertThat("$url$sep".appendIfMissing(sep), "appendIfMissing($url$sep)").isEqualTo("$url$sep")
}
@Test
fun testBold() {
assertThat(bold(1)).describedAs("bold(1)").isEqualTo(Colors.BOLD + "1" + Colors.BOLD)
assertThat(bold(2L)).describedAs("bold(1)").isEqualTo(Colors.BOLD + "2" + Colors.BOLD)
assertThat(bold(ascii)).describedAs("bold(ascii)").isEqualTo(Colors.BOLD + ascii + Colors.BOLD)
assertThat(bold("test")).describedAs("bold(test)").isEqualTo(Colors.BOLD + "test" + Colors.BOLD)
assertThat(bold(1), "bold(1)").isEqualTo(Colors.BOLD + "1" + Colors.BOLD)
assertThat(bold(2L), "bold(1)").isEqualTo(Colors.BOLD + "2" + Colors.BOLD)
assertThat(bold(ascii), "bold(ascii)").isEqualTo(Colors.BOLD + ascii + Colors.BOLD)
assertThat(bold("test"), "bold(test)").isEqualTo(Colors.BOLD + "test" + Colors.BOLD)
}
@Test
fun testBuildCmdSyntax() {
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")
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")
}
@Test
fun testCapitalise() {
assertThat("test".capitalise()).describedAs("capitalize(test)").isEqualTo("Test")
assertThat("Test".capitalise()).describedAs("capitalize(Test)").isEqualTo("Test")
assertThat(test.capitalise()).describedAs("capitalize($test)").isEqualTo(test)
assertThat("".capitalise()).describedAs("capitalize()").isEqualTo("")
assertThat("test".capitalise(), "capitalize(test)").isEqualTo("Test")
assertThat("Test".capitalise(), "capitalize(Test)").isEqualTo("Test")
assertThat(test.capitalise(), "capitalize($test)").isEqualTo(test)
assertThat("".capitalise(), "capitalize()").isEqualTo("")
}
@Test
fun textCapitaliseWords() {
assertThat(test.capitalizeWords()).describedAs("captiatlizeWords(test)").isEqualTo("This Is A Test.")
assertThat("Already Capitalized".capitalizeWords()).describedAs("already capitalized")
assertThat(test.capitalizeWords(), "captiatlizeWords(test)").isEqualTo("This Is A Test.")
assertThat("Already Capitalized".capitalizeWords(), "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
fun testColorize() {
assertThat(colorize(ascii, Colors.REVERSE)).describedAs("colorize(reverse)").isEqualTo(
assertThat(colorize(ascii, Colors.REVERSE), "colorize(reverse)").isEqualTo(
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)
assertThat(colorize(ascii, Colors.BOLD)).describedAs("colorized(bold)")
assertThat(colorize(ascii, Colors.BOLD), "colorized(bold)")
.isEqualTo(Colors.BOLD + ascii + Colors.BOLD)
assertThat(colorize(null, Colors.RED)).describedAs("colorize(null)").isEqualTo("")
assertThat(colorize("", Colors.RED)).describedAs("colorize()").isEqualTo("")
assertThat(colorize(ascii, DEFAULT_COLOR)).describedAs("colorize(none)").isEqualTo(ascii)
assertThat(colorize(" ", Colors.NORMAL)).describedAs("colorize(blank)")
assertThat(colorize(null, Colors.RED), "colorize(null)").isEqualTo("")
assertThat(colorize("", Colors.RED), "colorize()").isEqualTo("")
assertThat(colorize(ascii, DEFAULT_COLOR), "colorize(none)").isEqualTo(ascii)
assertThat(colorize(" ", Colors.NORMAL), "colorize(blank)")
.isEqualTo(Colors.NORMAL + " " + Colors.NORMAL)
}
@ -157,9 +158,9 @@ class UtilsTest {
val p = Properties()
p["one"] = "1"
p["two"] = "two"
assertThat(p.getIntProperty("one", 9)).describedAs("getIntProperty(one)").isEqualTo(1)
assertThat(p.getIntProperty("two", 2)).describedAs("getIntProperty(two)").isEqualTo(2)
assertThat(p.getIntProperty("foo", 3)).describedAs("getIntProperty(foo)").isEqualTo(3)
assertThat(p.getIntProperty("one", 9), "getIntProperty(one)").isEqualTo(1)
assertThat(p.getIntProperty("two", 2), "getIntProperty(two)").isEqualTo(2)
assertThat(p.getIntProperty("foo", 3), "getIntProperty(foo)").isEqualTo(3)
}
@Test
@ -169,26 +170,26 @@ class UtilsTest {
@Test
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}")
assertThat(helpFormat(test, isBold = false, isIndent = true)).describedAs("indent")
assertThat(helpFormat(test, isBold = false, isIndent = true), "indent")
.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())
}
@Test
fun testIsoLocalDate() {
assertThat(cal.time.toIsoLocalDate()).describedAs("isoLocalDate(date)").isEqualTo("1952-02-17")
assertThat(localDateTime.toIsoLocalDate()).describedAs("isoLocalDate(localDate)").isEqualTo("1952-02-17")
assertThat(cal.time.toIsoLocalDate(), "isoLocalDate(date)").isEqualTo("1952-02-17")
assertThat(localDateTime.toIsoLocalDate(), "isoLocalDate(localDate)").isEqualTo("1952-02-17")
}
@Test
fun testObfuscate() {
assertThat(ascii.obfuscate().length).describedAs("obfuscate is right length").isEqualTo(ascii.length)
assertThat(ascii.obfuscate()).describedAs("obfuscate()").isEqualTo("x".repeat(ascii.length))
assertThat(" ".obfuscate()).describedAs("obfuscate(blank)").isEqualTo(" ")
assertThat(ascii.obfuscate().length, "obfuscate is right length").isEqualTo(ascii.length)
assertThat(ascii.obfuscate(), "obfuscate()").isEqualTo("x".repeat(ascii.length))
assertThat(" ".obfuscate(), "obfuscate(blank)").isEqualTo(" ")
}
@Test
@ -197,7 +198,7 @@ class UtilsTest {
val weeks = "weeks"
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() {
val search = arrayOf("one", "two", "three")
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(","))
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"))
assertThat(test.replaceEach(search, emptyArray())).describedAs("replaceEach(search, empty)")
assertThat(test.replaceEach(search, emptyArray()), "replaceEach(search, empty)")
.isEqualTo(test)
}
@ -234,8 +235,8 @@ class UtilsTest {
@Test
fun testToIntOrDefault() {
assertThat("10".toIntOrDefault(1)).describedAs("toIntOrDefault(10, 1)").isEqualTo(10)
assertThat("a".toIntOrDefault(2)).describedAs("toIntOrDefault(a, 2)").isEqualTo(2)
assertThat("10".toIntOrDefault(1), "toIntOrDefault(10, 1)").isEqualTo(10)
assertThat("a".toIntOrDefault(2), "toIntOrDefault(a, 2)").isEqualTo(2)
}
@Test
@ -247,26 +248,26 @@ class UtilsTest {
@Test
fun testUptime() {
assertThat(uptime(547800300076L)).describedAs("full")
assertThat(uptime(547800300076L), "full")
.isEqualTo("17 years 2 months 2 weeks 1 day 6 hours 45 minutes")
assertThat(uptime(2700000L)).describedAs("minutes").isEqualTo("45 minutes")
assertThat(uptime(24300000L)).describedAs("hours minutes").isEqualTo("6 hours 45 minutes")
assertThat(uptime(110700000L)).describedAs("days hours minutes").isEqualTo("1 day 6 hours 45 minutes")
assertThat(uptime(1320300000L)).describedAs("weeks days hours minutes")
assertThat(uptime(2700000L), "minutes").isEqualTo("45 minutes")
assertThat(uptime(24300000L), "hours minutes").isEqualTo("6 hours 45 minutes")
assertThat(uptime(110700000L), "days hours minutes").isEqualTo("1 day 6 hours 45 minutes")
assertThat(uptime(1320300000L), "weeks days hours 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
@Throws(IOException::class)
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}")
}
@Test
fun testUtcDateTime() {
assertThat(cal.time.toUtcDateTime()).describedAs("utcDateTime(date)").isEqualTo("1952-02-17 12:30")
assertThat(localDateTime.toUtcDateTime()).describedAs("utcDateTime(localDate)").isEqualTo("1952-02-17 12:30")
assertThat(cal.time.toUtcDateTime(), "utcDateTime(date)").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
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 java.time.Duration
import java.time.LocalDateTime
@ -51,18 +56,21 @@ class TellMessageTest {
val recipient = "recipient"
val sender = "sender"
val tellMessage = TellMessage(sender, recipient, message)
assertThat(tellMessage).extracting("sender", "recipient", "message")
.containsExactly(sender, recipient, message)
assertThat(isValidDate(tellMessage.queued)).describedAs("queued is valid date/time").isTrue
assertThat(tellMessage.isMatch(sender)).describedAs("match sender").isTrue
assertThat(tellMessage.isMatch(recipient)).describedAs("match recipient").isTrue
assertThat(tellMessage.isMatch("foo")).describedAs("foo is no match").isFalse
assertThat(tellMessage).all {
prop(TellMessage::sender).isEqualTo(sender)
prop(TellMessage::recipient).isEqualTo(recipient)
prop(TellMessage::message).isEqualTo(message)
}
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
assertThat(tellMessage.receptionDate).describedAs("reception date not set").isEqualTo(LocalDateTime.MIN)
assertThat(tellMessage.receptionDate, "reception date not set").isEqualTo(LocalDateTime.MIN)
tellMessage.isReceived = true
assertThat(tellMessage.isReceived).describedAs("is received").isTrue
assertThat(isValidDate(tellMessage.receptionDate)).describedAs("received is valid date/time").isTrue
assertThat(tellMessage.isReceived, "is received").isTrue()
assertThat(isValidDate(tellMessage.receptionDate), "received is valid date/time").isTrue()
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
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.SyndCategoryImpl
import org.assertj.core.api.Assertions.assertThat
import org.testng.annotations.Test
import java.security.SecureRandom
import java.util.Date
@ -58,21 +65,30 @@ class EntryLinkTest {
entryLink.addComment("c$i", "u$i")
i++
}
assertThat(entryLink.comments.size).describedAs("getComments().size() == 5").isEqualTo(i)
assertThat(entryLink.comments.size, "getComments().size() == 5").isEqualTo(i)
i = 0
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++
}
val r = SecureRandom()
while (entryLink.comments.size > 0) {
entryLink.deleteComment(r.nextInt(entryLink.comments.size))
}
assertThat(entryLink.comments).describedAs("hasComments()").isEmpty()
assertThat(entryLink.comments, "hasComments()").isEmpty()
entryLink.addComment("nothing", "nobody")
entryLink.setComment(0, "something", "somebody")
assertThat(entryLink.getComment(0)).describedAs("get first comment").extracting("nick", "comment")
.containsExactly("somebody", "something")
val comment = entryLink.getComment(0)
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
@ -80,19 +96,19 @@ class EntryLinkTest {
val tag = "test"
val tags = listOf(SyndCategoryImpl().apply { name = tag })
val link = EntryLink("link", "title", "nick", "channel", Date(), tags)
assertThat(link.tags.size).describedAs("check tag size").isEqualTo(tags.size)
assertThat(link.tags[0].name).describedAs("check tag name").isEqualTo(tag)
assertThat(link.pinboardTags).describedAs("check pinboard tags").isEqualTo("nick,$tag")
assertThat(link.tags.size, "check tag size").isEqualTo(tags.size)
assertThat(link.tags[0].name, "check tag name").isEqualTo(tag)
assertThat(link.pinboardTags, "check pinboard tags").isEqualTo("nick,$tag")
}
@Test
fun testMatches() {
assertThat(entryLink.matches("mobitopia")).describedAs("match mobitopia").isTrue
assertThat(entryLink.matches("skynx")).describedAs("match nick").isTrue
assertThat(entryLink.matches("www.mobitopia.org")).describedAs("match url").isTrue
assertThat(entryLink.matches("foo")).describedAs("match foo").isFalse
assertThat(entryLink.matches("")).describedAs("match empty").isFalse
assertThat(entryLink.matches(null)).describedAs("match null").isFalse
assertThat(entryLink.matches("mobitopia"), "match mobitopia").isTrue()
assertThat(entryLink.matches("skynx"), "match nick").isTrue()
assertThat(entryLink.matches("www.mobitopia.org"), "match url").isTrue()
assertThat(entryLink.matches("foo"), "match foo").isFalse()
assertThat(entryLink.matches("<empty>"), "match empty").isFalse()
assertThat(entryLink.matches(null), "match null").isFalse()
}
@ -100,20 +116,19 @@ class EntryLinkTest {
fun testTags() {
val tags: List<SyndCategory> = entryLink.tags
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).describedAs("hasTags() is true").isNotEmpty
assertThat(entryLink.tags, "size is 5").size().isEqualTo(5)
entryLink.setTags("-tag5")
entryLink.setTags("+mobitopia")
entryLink.setTags("tag4")
entryLink.setTags("-mobitopia")
assertThat(entryLink.pinboardTags).describedAs("getPinboardTags()")
assertThat(entryLink.pinboardTags, "getPinboardTags()")
.isEqualTo(entryLink.nick + ",tag1,tag2,tag3,tag4,mobitopia")
val size = entryLink.tags.size
entryLink.setTags("")
assertThat(entryLink.tags.size).describedAs("empty tag").isEqualTo(size)
assertThat(entryLink.tags.size, "empty tag").isEqualTo(size)
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
import assertk.assertThat
import assertk.assertions.isEqualTo
import assertk.assertions.isFailure
import assertk.assertions.isInstanceOf
import net.objecthunter.exp4j.tokenizer.UnknownFunctionOrVariableException
import net.thauvin.erik.mobibot.Utils.bold
import net.thauvin.erik.mobibot.modules.Calc.Companion.calculate
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.testng.annotations.Test
/**
@ -44,11 +46,11 @@ import org.testng.annotations.Test
class CalcTest {
@Test
fun testCalculate() {
assertThat(calculate("1 + 1")).describedAs("calculate(1+1)").isEqualTo("1+1 = %s", bold(2))
assertThat(calculate("1 -3")).describedAs("calculate(1 -3)").isEqualTo("1-3 = %s", bold(-2))
assertThat(calculate("pi+π+e+φ")).describedAs("calculate(pi+π+e+φ)")
.isEqualTo("pi+π+e+φ = %s", bold("10.62"))
assertThatThrownBy { calculate("one + one") }.describedAs("calculate(one+one)")
.isInstanceOf(UnknownFunctionOrVariableException::class.java)
assertThat(calculate("1 + 1"), "calculate(1+1)").isEqualTo("1+1 = ${bold(2)}")
assertThat(calculate("1 -3"), "calculate(1 -3)").isEqualTo("1-3 = ${bold(-2)}")
assertThat(calculate("pi+π+e+φ"), "calculate(pi+π+e+φ)")
.isEqualTo("pi+π+e+φ = ${bold("10.62")}")
assertThat { calculate("one + one") }
.isFailure().isInstanceOf(UnknownFunctionOrVariableException::class.java)
}
}

View file

@ -31,8 +31,13 @@
*/
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 org.assertj.core.api.Assertions.assertThat
import org.testng.annotations.Test
/**
@ -43,11 +48,17 @@ class CryptoPricesTest {
@Throws(ModuleException::class)
fun testMarketPrice() {
var price = currentPrice(listOf("BTC"))
assertThat(price).extracting("base", "currency").containsExactly("BTC", "USD")
assertThat(price.amount.signum()).describedAs("BTC > 0").isGreaterThan(0)
assertThat(price, "BTC in USD").all {
prop(CryptoPrice::base).isEqualTo("BTC")
prop(CryptoPrice::currency).isEqualTo("USD")
prop(CryptoPrice::amount).transform { it.signum() }.isGreaterThan(0)
}
price = currentPrice(listOf("ETH", "EUR"))
assertThat(price).extracting("base", "currency").containsExactly("ETH", "EUR")
assertThat(price.amount.signum()).describedAs("ETH > 0").isGreaterThan(0)
assertThat(price, "ETH in EUR").all {
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
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.currencyRates
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.Test
@ -50,14 +61,28 @@ class CurrencyConverterTest {
@Test
fun testConvertCurrency() {
assertThat(convertCurrency("100 USD to EUR").msg)
.describedAs("100 USD to EUR").matches("\\$100\\.00 = €\\d{2,3}\\.\\d{2}")
assertThat(convertCurrency("100 USD to USD").msg).describedAs("100 USD to USD")
.contains("You're kidding, right?")
assertThat(convertCurrency("100 USD").msg).describedAs("100 USD").contains("Invalid query.")
assertThat(
convertCurrency("100 USD to EUR").msg,
"100 USD to EUR"
).matches("\\$100\\.00 = €\\d{2,3}\\.\\d{2}".toRegex())
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()
assertThat(rates.size).describedAs("currencyRates.size == 33").isEqualTo(33)
assertThat(rates).describedAs("currencyRates(EUR< USD)").contains("EUR: 1")
.anyMatch { it.matches("USD: .*".toRegex()) }
assertThat(rates).all {
size().isEqualTo(33)
any { it.matches("[A-Z]{3}: +[\\d.]+".toRegex()) }
contains("EUR: 1")
}
}
}

View file

@ -33,14 +33,15 @@
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
class DiceTest {
@Test
fun testWinLoseOrTie() {
assertThat(Dice.winLoseOrTie(6, 6)).describedAs("6 vs. 6").isEqualTo(Dice.Result.TIE)
assertThat(Dice.winLoseOrTie(6, 5)).describedAs("6 vs. 5").isEqualTo(Dice.Result.WIN)
assertThat(Dice.winLoseOrTie(5, 6)).describedAs("5 vs. 6").isEqualTo(Dice.Result.LOSE)
assertThat(Dice.winLoseOrTie(6, 6), "6 vs. 6").isEqualTo(Dice.Result.TIE)
assertThat(Dice.winLoseOrTie(6, 5), "6 vs. 5").isEqualTo(Dice.Result.WIN)
assertThat(Dice.winLoseOrTie(5, 6), "5 vs. 6").isEqualTo(Dice.Result.LOSE)
}
}

View file

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

View file

@ -31,8 +31,11 @@
*/
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 org.assertj.core.api.Assertions.assertThat
import org.testng.annotations.Test
/**
@ -42,7 +45,9 @@ class JokeTest {
@Test
@Throws(ModuleException::class)
fun testRandomJoke() {
assertThat(randomJoke().msg).describedAs("randomJoke() > 0").isNotEmpty
assertThat(randomJoke().msg).describedAs("randomJoke()").containsIgnoringCase("chuck")
assertThat(randomJoke().msg, "randomJoke() > 0").all {
isNotEmpty()
contains("chuck", true)
}
}
}

View file

@ -31,9 +31,11 @@
*/
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.whois
import org.assertj.core.api.Assertions.assertThat
import org.testng.annotations.Test
/**
@ -44,14 +46,13 @@ class LookupTest {
@Throws(Exception::class)
fun testLookup() {
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
@Throws(Exception::class)
fun testWhois() {
val result = whois("17.178.96.59", Lookup.WHOIS_HOST)
assertThat(result).describedAs("whois(17.178.96.59/Apple Inc.")
.anyMatch { it.contains("Apple Inc.") }
assertThat(result, "whois(17.178.96.59/Apple Inc.").any { it.contains("Apple Inc.") }
}
}

View file

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

View file

@ -31,8 +31,10 @@
*/
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 org.assertj.core.api.Assertions.assertThat
import org.testng.annotations.Test
/**
@ -41,13 +43,13 @@ import org.testng.annotations.Test
class PingTest {
@Test
fun testPingsArray() {
assertThat(Ping.PINGS).describedAs("Pings array is not empty.").isNotEmpty
assertThat(Ping.PINGS, "Pings array is not empty.").isNotEmpty()
}
@Test
fun testRandomPing() {
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
import org.assertj.core.api.Assertions.assertThat
import assertk.assertThat
import assertk.assertions.isEqualTo
import org.testng.annotations.Test
class RockPaperScissorsTest {
@Test
fun testWinLoseOrDraw() {
assertThat(RockPaperScissors.winLoseOrDraw("scissors", "paper")).describedAs("scissors vs. paper")
.isEqualTo("win")
assertThat(RockPaperScissors.winLoseOrDraw("paper", "rock")).describedAs("paper vs. rock").isEqualTo("win")
assertThat(RockPaperScissors.winLoseOrDraw("rock", "scissors")).describedAs("rock vs. scissors")
.isEqualTo("win")
assertThat(RockPaperScissors.winLoseOrDraw("paper", "scissors")).describedAs("paper vs. scissors")
.isEqualTo("lose")
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")
assertThat(RockPaperScissors.winLoseOrDraw("scissors", "paper"), "scissors vs. paper").isEqualTo("win")
assertThat(RockPaperScissors.winLoseOrDraw("paper", "rock"), "paper vs. rock").isEqualTo("win")
assertThat(RockPaperScissors.winLoseOrDraw("rock", "scissors"), "rock vs. scissors").isEqualTo("win")
assertThat(RockPaperScissors.winLoseOrDraw("paper", "scissors"), "paper vs. scissors").isEqualTo("lose")
assertThat(RockPaperScissors.winLoseOrDraw("rock", "paper"), "rock vs. paper").isEqualTo("lose")
assertThat(RockPaperScissors.winLoseOrDraw("scissors", "rock"), "scissors vs. rock").isEqualTo("lose")
assertThat(RockPaperScissors.winLoseOrDraw("scissors", "scissors"), "scissors vs. scissors").isEqualTo("draw")
}
}

View file

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

View file

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

View file

@ -31,6 +31,16 @@
*/
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.core.OWM
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.getWeather
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 kotlin.random.Random
@ -51,50 +59,63 @@ class Weather2Test : LocalProperties() {
@Test
fun testFtoC() {
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
fun testGetCountry() {
assertThat(getCountry("foo")).describedAs("not a country").isEqualTo(OWM.Country.UNITED_STATES)
assertThat(getCountry("fr")).describedAs("fr is france").isEqualTo(OWM.Country.FRANCE)
assertThat(getCountry("foo"), "not a country").isEqualTo(OWM.Country.UNITED_STATES)
assertThat(getCountry("fr"), "fr is france").isEqualTo(OWM.Country.FRANCE)
val country = OWM.Country.values()
repeat(3) {
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
fun testMphToKmh() {
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
@Throws(ModuleException::class)
fun testWeather() {
var messages = getWeather("98204", getProperty(OWM_API_KEY_PROP))
assertThat(messages[0].msg).describedAs("is Everett").contains("Everett, United States").contains("US")
assertThat(messages[messages.size - 1].msg).describedAs("is Everett zip code").endsWith("98204%2CUS")
assertThat(messages[0].msg, "is Everett").all {
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))
assertThat(messages[0].msg).describedAs("is San Francisco").contains("San Francisco").contains("US")
assertThat(messages[messages.size - 1].msg).describedAs("is San Fran city code").endsWith("5391959")
assertThat(messages[0].msg, "is San Francisco").all {
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))
assertThat(messages[0].msg).describedAs("is UK").contains("London, United Kingdom").contains("GB")
assertThat(messages[messages.size - 1].msg).describedAs("is London city code").endsWith("2643743")
assertThat(messages[0].msg, "is UK").all {
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)) }
.describedAs("foo not found").hasCauseInstanceOf(APIException::class.java)
assertThatThrownBy { getWeather("test", "") }
.describedAs("no API key").isInstanceOf(ModuleException::class.java).hasNoCause()
assertThatThrownBy { getWeather("test", null) }
.describedAs("null API key").isInstanceOf(ModuleException::class.java).hasNoCause()
try {
getWeather("Foo, US", getProperty(OWM_API_KEY_PROP))
} catch (e: ModuleException) {
assertThat(e.cause, "cause is API exception").isNotNull().isInstanceOf(APIException::class.java)
}
assertThat { getWeather("test", "") }.isFailure()
.isInstanceOf(ModuleException::class.java).hasNoCause()
assertThat { getWeather("test", null) }.isFailure()
.isInstanceOf(ModuleException::class.java).hasNoCause()
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
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.modules.WorldTime.Companion.BEATS_KEYWORD
import net.thauvin.erik.mobibot.modules.WorldTime.Companion.COUNTRIES_MAP
import net.thauvin.erik.mobibot.modules.WorldTime.Companion.time
import org.assertj.core.api.Assertions.assertThat
import org.pircbotx.Colors
import org.testng.annotations.Test
import java.time.ZoneId
import java.time.zone.ZoneRulesException
/**
* The `WordTimeTest` class.
@ -46,16 +50,23 @@ import java.time.zone.ZoneRulesException
class WordTimeTest {
@Test
fun testTime() {
assertThat(time("PST").msg).describedAs("PST").endsWith(bold("Los Angeles"))
assertThat(time("BLAH").isError).describedAs("BLAH").isTrue
assertThat(time("BEAT").msg).describedAs(BEATS_KEYWORD).matches("[\\w ]+: .?@\\d{3}+.? .beats")
assertThat(time(), "no zone").matches(
("The time is ${Colors.BOLD}\\d{1,2}:\\d{2}${Colors.BOLD} " +
"on ${Colors.BOLD}\\w+, \\d{1,2} \\w+ \\d{4}${Colors.BOLD} " +
"in ${Colors.BOLD}Los Angeles${Colors.BOLD}").toRegex()
)
assertThat(time(""), "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
@Throws(ZoneRulesException::class)
fun testCountries() {
fun testZones() {
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
import org.assertj.core.api.Assertions.assertThat
import assertk.assertThat
import assertk.assertions.isTrue
import org.testng.annotations.Test
class TestMessage {
@ -41,15 +42,15 @@ class TestMessage {
var msg = Message()
msg.isError = true
assertThat(msg.isNotice).describedAs("message is notice").isTrue
assertThat(msg.isNotice, "message is notice").isTrue()
msg = Message("foo", isError = true)
assertThat(msg.isNotice).describedAs("message is notice too").isTrue
assertThat(msg.isNotice, "message is notice too").isTrue()
}
@Test
fun testErrorMessage() {
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
#Wed Sep 15 17:27:25 PDT 2021
version.buildmeta=1400
#Mon Nov 08 13:50:12 PST 2021
version.buildmeta=2220
version.major=0
version.minor=8
version.patch=0
version.prerelease=beta
version.project=mobibot
version.semver=0.8.0-beta+1400
version.semver=0.8.0-beta+2220