-
\ No newline at end of file
diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml
deleted file mode 100644
index 0e29f96..0000000
--- a/.idea/jarRepositories.xml
+++ /dev/null
@@ -1,45 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
deleted file mode 100644
index e805548..0000000
--- a/.idea/kotlinc.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/libraries/bld.xml b/.idea/libraries/bld.xml
new file mode 100644
index 0000000..cf75013
--- /dev/null
+++ b/.idea/libraries/bld.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/libraries/compile.xml b/.idea/libraries/compile.xml
new file mode 100644
index 0000000..9bd86aa
--- /dev/null
+++ b/.idea/libraries/compile.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/libraries/runtime.xml b/.idea/libraries/runtime.xml
new file mode 100644
index 0000000..2ae5c4b
--- /dev/null
+++ b/.idea/libraries/runtime.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/libraries/test.xml b/.idea/libraries/test.xml
new file mode 100644
index 0000000..b80486a
--- /dev/null
+++ b/.idea/libraries/test.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
index a59e398..e853f87 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,16 +1,14 @@
+
-
-
-
+
+
+
+
-
-
-
-
-
-
-
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..55adcb9
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/runConfigurations/Run Tests.xml b/.idea/runConfigurations/Run Tests.xml
new file mode 100644
index 0000000..37dc742
--- /dev/null
+++ b/.idea/runConfigurations/Run Tests.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 0000000..c6500f2
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,11 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "type": "java",
+ "name": "Run Tests",
+ "request": "launch",
+ "mainClass": "net.thauvin.erik.MobibotTest"
+ }
+ ]
+}
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..133aa45
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,15 @@
+{
+ "java.project.sourcePaths": [
+ "src/main/java",
+ "src/main/resources",
+ "src/test/java",
+ "src/bld/java"
+ ],
+ "java.configuration.updateBuildConfiguration": "automatic",
+ "java.project.referencedLibraries": [
+ "${HOME}/.bld/dist/bld-1.7.5.jar",
+ "lib/compile/*.jar",
+ "lib/runtime/*.jar",
+ "lib/test/*.jar"
+ ]
+}
diff --git a/README.md b/README.md
index 1ecefeb..fbcacc2 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
# mobibot
[](https://opensource.org/licenses/BSD-3-Clause)
-[](https://kotlinlang.org)
+[](https://kotlinlang.org)
[](https://sonarcloud.io/summary/new_code?id=ethauvin_mobibot)
[](https://github.com/ethauvin/mobibot/actions/workflows/gradle.yml)
[](https://circleci.com/gh/ethauvin/mobibot/tree/master)
@@ -14,8 +14,8 @@ Some very basic instructions:
cd mobibot
- # build with gradle
- ./gradlew
+ # build JAR and deploy
+ ./bld jar deploy
cd deploy
diff --git a/bin/main/log4j2.xml b/bin/main/log4j2.xml
new file mode 100644
index 0000000..265c88f
--- /dev/null
+++ b/bin/main/log4j2.xml
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bin/main/net/thauvin/erik/mobibot/Addons.kt b/bin/main/net/thauvin/erik/mobibot/Addons.kt
new file mode 100644
index 0000000..2c5f05d
--- /dev/null
+++ b/bin/main/net/thauvin/erik/mobibot/Addons.kt
@@ -0,0 +1,190 @@
+/*
+ * Addons.kt
+ *
+ * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net)
+ *
+ * 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
+
+import net.thauvin.erik.mobibot.Utils.notContains
+import net.thauvin.erik.mobibot.commands.AbstractCommand
+import net.thauvin.erik.mobibot.commands.links.LinksManager
+import net.thauvin.erik.mobibot.modules.AbstractModule
+import org.pircbotx.hooks.events.PrivateMessageEvent
+import org.pircbotx.hooks.types.GenericMessageEvent
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+import java.util.*
+
+/**
+ * Modules and Commands addons.
+ */
+class Addons(private val props: Properties) {
+ private val logger: Logger = LoggerFactory.getLogger(Addons::class.java)
+ private val disabledModules = props.getProperty("disabled-modules", "").split(LinksManager.TAG_MATCH)
+ private val disableCommands = props.getProperty("disabled-commands", "").split(LinksManager.TAG_MATCH)
+
+ val commands: MutableList = mutableListOf()
+ val modules: MutableList = mutableListOf()
+ val names = Names
+
+ /**
+ * Add a module with properties.
+ */
+ fun add(module: AbstractModule): Boolean {
+ var enabled = false
+ with(module) {
+ if (disabledModules.notContains(name, true)) {
+ if (hasProperties()) {
+ propertyKeys.forEach {
+ setProperty(it, props.getProperty(it, ""))
+ }
+ }
+
+ if (isEnabled) {
+ modules.add(this)
+ names.modules.add(name)
+ names.commands.addAll(commands)
+ enabled = true
+ } else {
+ if (logger.isDebugEnabled) {
+ logger.debug("Module $name is disabled.")
+ }
+ names.disabledModules.add(name)
+ }
+ } else {
+ names.disabledModules.add(name)
+ }
+ }
+ return enabled
+ }
+
+ /**
+ * Add a command with properties.
+ */
+ fun add(command: AbstractCommand): Boolean {
+ var enabled = false
+ with(command) {
+ if (disableCommands.notContains(name, true)) {
+ if (properties.isNotEmpty()) {
+ properties.keys.forEach {
+ setProperty(it, props.getProperty(it, ""))
+ }
+ }
+ if (isEnabled()) {
+ commands.add(this)
+ if (isVisible) {
+ if (isOpOnly) {
+ names.ops.add(name)
+ } else {
+ names.commands.add(name)
+ }
+ }
+ enabled = true
+ } else {
+ if (logger.isDebugEnabled) {
+ logger.debug("Command $name is disabled.")
+ }
+ names.disabledCommands.add(name)
+ }
+ } else {
+ names.disabledCommands.add(name)
+ }
+ }
+ return enabled
+ }
+
+ /**
+ * Execute a command or module.
+ */
+ 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(channel, args, event)
+ return true
+ }
+ }
+ val mods = if (event is PrivateMessageEvent) modules.filter { it.isPrivateMsgEnabled } else modules
+ for (module in mods) {
+ if (module.commands.contains(cmd)) {
+ module.commandResponse(channel, cmd, args, event)
+ return true
+ }
+ }
+ return false
+ }
+
+ /**
+ * Match a command.
+ */
+ fun match(channel: String, event: GenericMessageEvent): Boolean {
+ for (command in commands) {
+ if (command.matches(event.message)) {
+ command.commandResponse(channel, event.message, event)
+ return true
+ }
+ }
+ return false
+ }
+
+ /**
+ * Commands and Modules help.
+ */
+ fun help(channel: String, topic: String, event: GenericMessageEvent): Boolean {
+ for (command in commands) {
+ if (command.isVisible && command.name.startsWith(topic)) {
+ return command.helpResponse(channel, topic, event)
+ }
+ }
+ for (module in modules) {
+ if (module.commands.contains(topic)) {
+ return module.helpResponse(event)
+ }
+ }
+ return false
+ }
+
+ /**
+ * Holds commands and modules names.
+ */
+ object Names {
+ val modules: MutableList = mutableListOf()
+ val disabledModules: MutableList = mutableListOf()
+ val commands: MutableList = mutableListOf()
+ val disabledCommands: MutableList = mutableListOf()
+ val ops: MutableList = mutableListOf()
+
+ fun sort() {
+ modules.sort()
+ disabledModules.sort()
+ commands.sort()
+ disabledCommands.sort()
+ ops.sort()
+ }
+ }
+}
diff --git a/bin/main/net/thauvin/erik/mobibot/Constants.kt b/bin/main/net/thauvin/erik/mobibot/Constants.kt
new file mode 100644
index 0000000..98ef74a
--- /dev/null
+++ b/bin/main/net/thauvin/erik/mobibot/Constants.kt
@@ -0,0 +1,102 @@
+/*
+ * Constants.kt
+ *
+ * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net)
+ *
+ * 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
+
+/**
+ * The `Constants`.
+ */
+object Constants {
+ /**
+ * The connect/read timeout in ms.
+ */
+ const val CONNECT_TIMEOUT = 5000
+
+ /**
+ * Debug command line argument.
+ */
+ const val DEBUG_ARG = "debug"
+
+ /**
+ * Default IRC Port.
+ */
+ const val DEFAULT_PORT = 6667
+
+ /**
+ * Default IRC Server.
+ */
+ const val DEFAULT_SERVER = "irc.libera.chat"
+
+ /**
+ * CLI command for usage.
+ */
+ const val CLI_CMD = "java -jar ${ReleaseInfo.PROJECT}.jar"
+
+ /**
+ * User-Agent
+ */
+ const val USER_AGENT =
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36"
+
+ /**
+ * The help command.
+ */
+ const val HELP_CMD = "help"
+
+ /**
+ * The link command.
+ */
+ const val LINK_CMD = "L"
+
+ /**
+ * The empty title string.
+ */
+ const val NO_TITLE = "No Title"
+
+ /**
+ * Properties command line argument.
+ */
+ const val PROPS_ARG = "properties"
+
+ /**
+ * The tag command
+ */
+ const val TAG_CMD = "T"
+
+ /**
+ * The timer delay in minutes.
+ */
+ const val TIMER_DELAY = 10L
+
+ /**
+ * Properties version line argument.
+ */
+ const val VERSION_ARG = "version"
+}
diff --git a/bin/main/net/thauvin/erik/mobibot/FeedReader.kt b/bin/main/net/thauvin/erik/mobibot/FeedReader.kt
new file mode 100644
index 0000000..d82f011
--- /dev/null
+++ b/bin/main/net/thauvin/erik/mobibot/FeedReader.kt
@@ -0,0 +1,92 @@
+/*
+ * FeedReader.kt
+ *
+ * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net)
+ *
+ * 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
+
+import com.rometools.rome.io.FeedException
+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.FeedsManager
+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 java.io.IOException
+import java.net.URL
+
+/**
+ * Reads an RSS feed.
+ */
+class FeedReader(private val url: String, val event: GenericMessageEvent) : Runnable {
+ private val logger: Logger = LoggerFactory.getLogger(FeedsManager::class.java)
+
+ /**
+ * Fetches the Feed's items.
+ */
+ override fun run() {
+ try {
+ readFeed(url).forEach {
+ event.sendMessage("", it)
+ }
+ } catch (e: FeedException) {
+ if (logger.isWarnEnabled) logger.warn("Unable to parse the feed at $url", e)
+ event.sendMessage("An error has occurred while parsing the feed: ${e.message}")
+ } catch (e: IOException) {
+ if (logger.isWarnEnabled) logger.warn("Unable to fetch the feed at $url", e)
+ event.sendMessage("An IO error has occurred while fetching the feed: ${e.message}")
+ }
+ }
+
+ companion object {
+ @JvmStatic
+ @Throws(FeedException::class, IOException::class)
+ fun readFeed(url: String, maxItems: Int = 5): List {
+ val messages = mutableListOf()
+ val input = SyndFeedInput()
+ XmlReader(URL(url).openStream()).use { reader ->
+ val feed = input.build(reader)
+ val items = feed.entries
+ if (items.isEmpty()) {
+ messages.add(NoticeMessage("There is currently nothing to view."))
+ } else {
+ items.take(maxItems).forEach {
+ messages.add(NoticeMessage(it.title))
+ messages.add(NoticeMessage(helpFormat(it.link.green(), false)))
+ }
+ }
+ }
+ return messages
+ }
+ }
+}
diff --git a/bin/main/net/thauvin/erik/mobibot/Mobibot.kt b/bin/main/net/thauvin/erik/mobibot/Mobibot.kt
new file mode 100644
index 0000000..f91c457
--- /dev/null
+++ b/bin/main/net/thauvin/erik/mobibot/Mobibot.kt
@@ -0,0 +1,421 @@
+/*
+ * Mobibot.kt
+ *
+ * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net)
+ *
+ * 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
+
+import kotlinx.cli.ArgParser
+import kotlinx.cli.ArgType
+import kotlinx.cli.default
+import net.thauvin.erik.mobibot.Utils.appendIfMissing
+import net.thauvin.erik.mobibot.Utils.bot
+import net.thauvin.erik.mobibot.Utils.capitalise
+import net.thauvin.erik.mobibot.Utils.getIntProperty
+import net.thauvin.erik.mobibot.Utils.helpCmdSyntax
+import net.thauvin.erik.mobibot.Utils.helpFormat
+import net.thauvin.erik.mobibot.Utils.isChannelOp
+import net.thauvin.erik.mobibot.Utils.lastOrEmpty
+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.commands.*
+import net.thauvin.erik.mobibot.commands.Recap.Companion.storeRecap
+import net.thauvin.erik.mobibot.commands.links.*
+import net.thauvin.erik.mobibot.commands.seen.Seen
+import net.thauvin.erik.mobibot.commands.tell.Tell
+import net.thauvin.erik.mobibot.modules.*
+import net.thauvin.erik.semver.Version
+import org.pircbotx.Configuration
+import org.pircbotx.PircBotX
+import org.pircbotx.hooks.ListenerAdapter
+import org.pircbotx.hooks.events.*
+import org.pircbotx.hooks.types.GenericMessageEvent
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+import java.io.*
+import java.nio.file.Files
+import java.nio.file.Paths
+import java.util.*
+import java.util.regex.Pattern
+import kotlin.system.exitProcess
+
+@Version(properties = "version.properties", className = "ReleaseInfo", template = "version.mustache", type = "kt")
+class Mobibot(nickname: String, val channel: String, logsDirPath: String, p: Properties) : ListenerAdapter() {
+ // The bot configuration.
+ private val config: Configuration
+
+ // Commands and Modules
+ private val addons: Addons
+
+ // Seen command
+ private val seen: Seen
+
+ // Tell command
+ private val tell: Tell
+
+ /** Logger. */
+ val logger: Logger = LoggerFactory.getLogger(Mobibot::class.java)
+
+ /**
+ * Connects to the server and joins the channel.
+ */
+ fun connect() {
+ PircBotX(config).startBot()
+ }
+
+ /**
+ * Responds with the default help.
+ */
+ private fun helpDefault(event: GenericMessageEvent) {
+ event.sendMessage("Type a URL on $channel to post it.")
+ event.sendMessage("For more information on a specific command, type:")
+ event.sendMessage(
+ helpFormat(
+ helpCmdSyntax("%c ${Constants.HELP_CMD} ", event.bot().nick, event is PrivateMessageEvent)
+ )
+ )
+ event.sendMessage("The commands are:")
+ event.sendList(addons.names.commands, 8, isBold = true, isIndent = true)
+ if (event.isChannelOp(channel)) {
+ if (addons.names.disabledCommands.isNotEmpty()) {
+ event.sendMessage("The disabled commands are:")
+ event.sendList(addons.names.disabledCommands, 8, isBold = false, isIndent = true)
+ }
+ event.sendMessage("The op commands are:")
+ event.sendList(addons.names.ops, 8, isBold = true, isIndent = true)
+ }
+ }
+
+ /**
+ * Responds with the default, commands or modules help.
+ */
+ private fun helpResponse(event: GenericMessageEvent, topic: String) {
+ if (topic.isBlank() || !addons.help(channel, topic.lowercase().trim(), event)) {
+ helpDefault(event)
+ }
+ }
+
+ override fun onAction(event: ActionEvent?) {
+ event?.channel?.let {
+ if (channel == it.name) {
+ event.user?.let { user ->
+ storeRecap(user.nick, event.action, true)
+ }
+ }
+ }
+ }
+
+ override fun onDisconnect(event: DisconnectEvent?) {
+ event?.let {
+ with(event.getBot()) {
+ LinksManager.socialManager.notification("$nick disconnected from $serverHostname")
+ seen.add(userChannelDao.getChannel(channel).users)
+ }
+ }
+ LinksManager.socialManager.shutdown()
+ }
+
+ override fun onPrivateMessage(event: PrivateMessageEvent?) {
+ event?.user?.let { user ->
+ if (logger.isTraceEnabled) logger.trace("<<< ${user.nick}: ${event.message}")
+ val cmds = event.message.trim().split(" ".toRegex(), 2)
+ val cmd = cmds[0].lowercase()
+ val args = cmds.lastOrEmpty().trim()
+ if (cmd.startsWith(Constants.HELP_CMD)) { // help
+ helpResponse(event, args)
+ } else if (!addons.exec(channel, cmd, args, event)) { // Execute command or module
+ helpDefault(event)
+ }
+ }
+ }
+
+ override fun onJoin(event: JoinEvent?) {
+ event?.user?.let { user ->
+ with(event.getBot()) {
+ if (user.nick == nick) {
+ LinksManager.socialManager.notification(
+ "$nick has joined ${event.channel.name} on $serverHostname"
+ )
+ seen.add(userChannelDao.getChannel(channel).users)
+ } else {
+ tell.send(event)
+ seen.add(user.nick)
+ }
+ }
+ }
+ }
+
+ override fun onMessage(event: MessageEvent?) {
+ event?.user?.let { user ->
+ tell.send(event)
+ if (event.message.matches("(?i)${Pattern.quote(event.bot().nick)}:.*".toRegex())) { // mobibot:
+ if (logger.isTraceEnabled) logger.trace(">>> ${user.nick}: ${event.message}")
+ val cmds = event.message.substring(event.bot().nick.length + 1).trim().split(" ".toRegex(), 2)
+ val cmd = cmds[0].lowercase()
+ val args = cmds.lastOrEmpty().trim()
+ if (cmd.startsWith(Constants.HELP_CMD)) { // mobibot: help
+ helpResponse(event, args)
+ } else {
+ // Execute module or command
+ addons.exec(channel, cmd, args, event)
+ }
+ } else if (addons.match(channel, event)) { // Links, e.g.: https://www.example.com/ or L1: , etc.
+ if (logger.isTraceEnabled) logger.trace(">>> ${user.nick}: ${event.message}")
+ }
+ storeRecap(user.nick, event.message, false)
+ seen.add(user.nick)
+ }
+ }
+
+ override fun onNickChange(event: NickChangeEvent?) {
+ event?.let {
+ tell.send(event)
+ if (!it.oldNick.equals(it.newNick, true)) {
+ seen.add(it.oldNick)
+ }
+ seen.add(it.newNick)
+ }
+ }
+
+ override fun onPart(event: PartEvent?) {
+ event?.user?.let { user ->
+ with(event.getBot()) {
+ if (user.nick == nick) {
+ LinksManager.socialManager.notification(
+ "$nick has left ${event.channel.name} on $serverHostname"
+ )
+ seen.add(userChannelDao.getChannel(channel).users)
+ } else {
+ seen.add(user.nick)
+ }
+ }
+ }
+ }
+
+ override fun onQuit(event: QuitEvent?) {
+ event?.user?.let { user ->
+ seen.add(user.nick)
+ }
+ }
+
+ companion object {
+ @JvmStatic
+ @Throws(Exception::class)
+ fun main(args: Array) {
+ // Set up the command line options
+ val parser = ArgParser(Constants.CLI_CMD)
+ val debug by parser.option(
+ ArgType.Boolean,
+ Constants.DEBUG_ARG,
+ Constants.DEBUG_ARG.substring(0, 1),
+ "Print debug & logging data directly to the console"
+ ).default(false)
+ val property by parser.option(
+ ArgType.String,
+ Constants.PROPS_ARG,
+ Constants.PROPS_ARG.substring(0, 1),
+ "Use alternate properties file"
+ ).default("./${ReleaseInfo.PROJECT}.properties")
+ val version by parser.option(
+ ArgType.Boolean,
+ Constants.VERSION_ARG,
+ Constants.VERSION_ARG.substring(0, 1),
+ "Print version info"
+ ).default(false)
+
+ // Parse the command line
+ parser.parse(args)
+
+ if (version) {
+ // Output the version
+ println(
+ "${ReleaseInfo.PROJECT.capitalise()} ${ReleaseInfo.VERSION}" +
+ " (${ReleaseInfo.BUILDDATE.toIsoLocalDate()})"
+ )
+ println(ReleaseInfo.WEBSITE)
+ } else {
+ // Load the properties
+ val p = Properties()
+ try {
+ Files.newInputStream(
+ Paths.get(property)
+ ).use { fis ->
+ p.load(fis)
+ }
+ } catch (ignore: FileNotFoundException) {
+ System.err.println("Unable to find properties file.")
+ exitProcess(1)
+ } catch (ignore: IOException) {
+ System.err.println("Unable to open properties file.")
+ exitProcess(1)
+ }
+ val nickname = p.getProperty("nick", Mobibot::class.java.name.lowercase())
+ val channel = p.getProperty("channel")
+ val logsDir = p.getProperty("logs", ".").appendIfMissing(File.separatorChar)
+
+ // Redirect stdout and stderr
+ if (!debug) {
+ try {
+ val stdout = PrintStream(
+ BufferedOutputStream(
+ FileOutputStream(
+ logsDir + channel.substring(1) + '.' + Utils.today() + ".log", true
+ )
+ ), true
+ )
+ System.setOut(stdout)
+ } catch (ignore: IOException) {
+ System.err.println("Unable to open output (stdout) log file.")
+ exitProcess(1)
+ }
+ try {
+ val stderr = PrintStream(
+ BufferedOutputStream(
+ FileOutputStream("$logsDir$nickname.err", true)
+ ), true
+ )
+ System.setErr(stderr)
+ } catch (ignore: IOException) {
+ System.err.println("Unable to open error (stderr) log file.")
+ exitProcess(1)
+ }
+ }
+
+ // Start the bot
+ Mobibot(nickname, channel, logsDir, p).connect()
+ }
+ }
+ }
+
+ /**
+ * Initialize the bot.
+ */
+ init {
+ 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()
+
+ // Load the current entries
+ with(LinksManager) {
+ entries.channel = channel
+ entries.ircServer = ircServer
+ entries.logsDir = logsDirPath
+ entries.backlogs = p.getProperty("backlogs", "")
+ entries.load()
+
+ // Set up pinboard
+ pinboard.setApiToken(p.getProperty("pinboard-api-token", ""))
+ }
+
+ addons = Addons(p)
+
+ // Load the commands
+ addons.add(ChannelFeed(channel.removePrefix("#")))
+ addons.add(Comment())
+ addons.add(Cycle())
+ addons.add(Die())
+ addons.add(Ignore())
+ addons.add(LinksManager())
+ addons.add(Me())
+ addons.add(Modules(addons.names.modules, addons.names.disabledModules))
+ addons.add(Msg())
+ addons.add(Nick())
+ addons.add(Posting())
+ addons.add(Recap())
+ addons.add(Say())
+
+ // Seen command
+ seen = Seen("${logsDirPath}${nickname}-seen.ser")
+ addons.add(seen)
+
+ addons.add(Tags())
+
+ // Tell command
+ tell = Tell("${logsDirPath}${nickname}.ser")
+ addons.add(tell)
+
+ addons.add(Users())
+ addons.add(Versions())
+ addons.add(View())
+
+ // Load social modules
+ LinksManager.socialManager.add(addons, Mastodon())
+
+ // Load the modules
+ addons.add(Calc())
+ addons.add(ChatGpt())
+ addons.add(CryptoPrices())
+ addons.add(CurrencyConverter())
+ addons.add(Dice())
+ addons.add(GoogleSearch())
+ addons.add(Info(tell, seen))
+ addons.add(Joke())
+ addons.add(Lookup())
+ addons.add(Ping())
+ addons.add(RockPaperScissors())
+ addons.add(StockQuote())
+ addons.add(War())
+ addons.add(Weather2())
+ addons.add(WolframAlpha())
+ addons.add(WorldTime())
+
+ // Sort the addons
+ addons.names.sort()
+ }
+}
+
diff --git a/bin/main/net/thauvin/erik/mobibot/Pinboard.kt b/bin/main/net/thauvin/erik/mobibot/Pinboard.kt
new file mode 100644
index 0000000..7cb5aed
--- /dev/null
+++ b/bin/main/net/thauvin/erik/mobibot/Pinboard.kt
@@ -0,0 +1,113 @@
+/*
+ * Pinboard.kt
+ *
+ * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net)
+ *
+ * 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
+
+import net.thauvin.erik.mobibot.entries.EntryLink
+import net.thauvin.erik.pinboard.PinboardPoster
+import java.time.ZoneId
+import java.time.ZonedDateTime
+import java.time.format.DateTimeFormatter
+import java.time.temporal.ChronoUnit
+import java.util.*
+
+/**
+ * Handles posts to pinboard.in.
+ */
+class Pinboard {
+ private val poster = PinboardPoster()
+
+ /**
+ * Adds a pin.
+ */
+ fun addPin(ircServer: String, entry: EntryLink) {
+ if (poster.apiToken.isNotBlank()) {
+ with(entry) {
+ poster.addPin(link, title, postedBy(ircServer), formatTags(), date.toTimestamp())
+ }
+ }
+ }
+
+ /**
+ * Sets the pinboard API token.
+ */
+ fun setApiToken(apiToken: String) {
+ poster.apiToken = apiToken
+ }
+
+ /**
+ * Deletes a pin.
+ */
+ fun deletePin(entry: EntryLink) {
+ if (poster.apiToken.isNotBlank()) {
+ poster.deletePin(entry.link)
+ }
+
+ }
+
+ /**
+ * Updates a pin.
+ */
+ fun updatePin(ircServer: String, oldUrl: String, entry: EntryLink) {
+ if (poster.apiToken.isNotBlank()) {
+ with(entry) {
+ if (oldUrl != link) {
+ poster.deletePin(oldUrl)
+ }
+ poster.addPin(link, title, postedBy(ircServer), formatTags(), date.toTimestamp())
+ }
+ }
+ }
+
+ /**
+ * Formats a date to a UTC timestamp.
+ */
+ private fun Date.toTimestamp(): String {
+ return ZonedDateTime.ofInstant(
+ toInstant().truncatedTo(ChronoUnit.SECONDS), ZoneId.systemDefault()
+ ).format(DateTimeFormatter.ISO_INSTANT)
+ }
+
+ /**
+ * Formats the tags for pinboard.
+ */
+ private fun EntryLink.formatTags(): String {
+ return nick + formatTags(",", ",")
+ }
+
+ /**
+ * Returns the pinboard.in extended attribution line.
+ */
+ private fun EntryLink.postedBy(ircServer: String): String {
+ return "Posted by $nick on $channel ( $ircServer )"
+ }
+}
+
diff --git a/bin/main/net/thauvin/erik/mobibot/Utils.kt b/bin/main/net/thauvin/erik/mobibot/Utils.kt
new file mode 100644
index 0000000..e4760d2
--- /dev/null
+++ b/bin/main/net/thauvin/erik/mobibot/Utils.kt
@@ -0,0 +1,439 @@
+/*
+ * Utils.kt
+ *
+ * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net)
+ *
+ * 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
+
+import net.thauvin.erik.mobibot.msg.Message
+import net.thauvin.erik.mobibot.msg.Message.Companion.DEFAULT_COLOR
+import net.thauvin.erik.urlencoder.UrlEncoderUtil
+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 org.slf4j.Logger
+import java.io.*
+import java.net.HttpURLConnection
+import java.net.URL
+import java.nio.file.Files
+import java.nio.file.Paths
+import java.time.LocalDateTime
+import java.time.ZoneId
+import java.time.format.DateTimeFormatter
+import java.util.*
+import kotlin.io.path.exists
+import kotlin.io.path.fileSize
+
+/**
+ * Miscellaneous utilities.
+ */
+@Suppress("TooManyFunctions")
+object Utils {
+ private val searchFlags = arrayOf("%c", "%n")
+
+ /**
+ * Prepends a prefix if not present.
+ */
+ @JvmStatic
+ fun String.prefixIfMissing(prefix: Char): String {
+ return if (first() != prefix) {
+ "$prefix${this}"
+ } else {
+ this
+ }
+ }
+
+ /**
+ * Appends a suffix to the end of the String if not present.
+ */
+ @JvmStatic
+ fun String.appendIfMissing(suffix: Char): String {
+ return if (last() != suffix) {
+ "$this${suffix}"
+ } else {
+ this
+ }
+ }
+
+ /**
+ * Makes the given int bold.
+ */
+ @JvmStatic
+ fun Int.bold(): String = toString().bold()
+
+ /**
+ * Makes the given long bold.
+ */
+ @JvmStatic
+ fun Long.bold(): String = toString().bold()
+
+ /**
+ * Makes the given string bold.
+ */
+ @JvmStatic
+ fun String?.bold(): String = colorize(Colors.BOLD)
+
+ /**
+ * Returns the [PircBotX] instance.
+ */
+ fun GenericMessageEvent.bot(): PircBotX {
+ return getBot() as PircBotX
+ }
+
+ /**
+ * Capitalize a string.
+ */
+ @JvmStatic
+ fun String.capitalise(): String = lowercase().replaceFirstChar { it.uppercase() }
+
+ /**
+ * Capitalize words
+ */
+ @JvmStatic
+ fun String.capitalizeWords(): String = split(" ").joinToString(" ") { it.capitalise() }
+
+ /**
+ * Colorize a string.
+ */
+ @JvmStatic
+ fun String?.colorize(color: String): String {
+ return when {
+ isNullOrEmpty() -> {
+ ""
+ }
+
+ color == DEFAULT_COLOR -> {
+ this
+ }
+
+ Colors.BOLD == color || Colors.REVERSE == color -> {
+ color + this + color
+ }
+
+ else -> {
+ color + this + Colors.NORMAL
+ }
+ }
+ }
+
+ /**
+ * Makes the given string cyan.
+ */
+ @JvmStatic
+ fun String?.cyan(): String = colorize(Colors.CYAN)
+
+ /**
+ * URL encodes the given string.
+ */
+ @JvmStatic
+ fun String.encodeUrl(): String = UrlEncoderUtil.encode(this)
+
+ /**
+ * Returns a property as an int.
+ */
+ @JvmStatic
+ fun Properties.getIntProperty(key: String, defaultValue: Int): Int {
+ return getProperty(key)?.toIntOrDefault(defaultValue) ?: defaultValue
+ }
+
+ /**
+ * Makes the given string green.
+ */
+ @JvmStatic
+ fun String?.green(): String = colorize(Colors.DARK_GREEN)
+
+ /**
+ * Build a help command by replacing `%c` with the bot's pub/priv command, and `%n` with the bot's
+ * nick.
+ */
+ @JvmStatic
+ fun helpCmdSyntax(text: String, botNick: String, isPrivate: Boolean): String {
+ val replace = arrayOf(if (isPrivate) "/msg $botNick" else "$botNick:", botNick)
+ return text.replaceEach(searchFlags, replace)
+ }
+
+ /**
+ * Returns a formatted help string.
+ */
+ @JvmStatic
+ @JvmOverloads
+ fun helpFormat(help: String, isBold: Boolean = true, isIndent: Boolean = true): String {
+ val s = if (isBold) help.bold() else help
+ return if (isIndent) s.prependIndent() else s
+ }
+
+ /**
+ * Returns `true` if the specified user is an operator on the [channel].
+ */
+ @JvmStatic
+ fun GenericMessageEvent.isChannelOp(channel: String): Boolean {
+ return this.bot().userChannelDao.getChannel(channel).isOp(this.user)
+ }
+
+ /**
+ * Returns `true` if a HTTP status code indicates a successful response.
+ */
+ @JvmStatic
+ fun Int.isHttpSuccess() = this in 200..399
+
+ /**
+ * Returns the last item of a list of strings or empty if none.
+ */
+ @JvmStatic
+ fun List.lastOrEmpty(): String {
+ return if (this.size >= 2) {
+ this.last()
+ } else
+ ""
+ }
+
+ /**
+ * Load serial data from file.
+ */
+ @JvmStatic
+ fun loadSerialData(file: String, default: Any, logger: Logger, description: String): Any {
+ val serialFile = Paths.get(file)
+ if (serialFile.exists() && serialFile.fileSize() > 0) {
+ try {
+ ObjectInputStream(
+ BufferedInputStream(Files.newInputStream(serialFile))
+ ).use { input ->
+ if (logger.isDebugEnabled) logger.debug("Loading the ${description}.")
+ return input.readObject()
+ }
+ } catch (e: IOException) {
+ logger.error("An IO error occurred loading the ${description}.", e)
+ } catch (e: ClassNotFoundException) {
+ logger.error("An error occurred loading the ${description}.", e)
+ }
+ }
+ return default
+ }
+
+ /**
+ * Returns `true` if the list does not contain the given string.
+ */
+ @JvmStatic
+ fun List.notContains(text: String, ignoreCase: Boolean = false) = this.none { it.equals(text, ignoreCase) }
+
+ /**
+ * Obfuscates the given string.
+ */
+ @JvmStatic
+ fun String.obfuscate(): String {
+ return if (isNotBlank()) {
+ "x".repeat(length)
+ } else this
+ }
+
+ /**
+ * Returns the plural form of a word, if count > 1.
+ */
+ @JvmStatic
+ fun String.plural(count: Long): String {
+ return if (count > 1) "${this}s" else this
+ }
+
+ /**
+ * Makes the given string red.
+ */
+ @JvmStatic
+ fun String?.red(): String = colorize(Colors.RED)
+
+ /**
+ * Replaces all occurrences of Strings within another String.
+ */
+ @JvmStatic
+ fun String.replaceEach(search: Array, replace: Array): String {
+ var result = this
+ if (search.size == replace.size) {
+ search.forEachIndexed { i, s ->
+ result = result.replace(s, replace[i])
+ }
+ }
+ return result
+ }
+
+ /**
+ * Makes the given string reverse color.
+ */
+ @JvmStatic
+ fun String?.reverseColor(): String = colorize(Colors.REVERSE)
+
+ /**
+ * Save data
+ */
+ @JvmStatic
+ fun saveSerialData(file: String, data: Any, logger: Logger, description: String) {
+ try {
+ BufferedOutputStream(Files.newOutputStream(Paths.get(file))).use { bos ->
+ ObjectOutputStream(bos).use { output ->
+ if (logger.isDebugEnabled) logger.debug("Saving the ${description}.")
+ output.writeObject(data)
+ }
+ }
+ } catch (e: IOException) {
+ logger.error("Unable to save the ${description}.", e)
+ }
+ }
+
+ /**
+ * Send a formatted commands/modules, etc. list.
+ */
+ @JvmStatic
+ @JvmOverloads
+ fun GenericMessageEvent.sendList(
+ list: List,
+ 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, message.msg.colorize(message.color))
+ } else if (message.isPrivate || this is PrivateMessageEvent || channel.isBlank()) {
+ respondPrivateMessage(message.msg.colorize(message.color))
+ } else {
+ bot().sendIRC().message(channel, message.msg.colorize(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.
+ */
+ @JvmStatic
+ fun today(): String = LocalDateTime.now().toIsoLocalDate()
+
+ /**
+ * Converts a string to an int.
+ */
+ @JvmStatic
+ fun String.toIntOrDefault(defaultValue: Int): Int {
+ return try {
+ toInt()
+ } catch (e: NumberFormatException) {
+ defaultValue
+ }
+ }
+
+ /**
+ * Returns the specified date as an ISO local date string.
+ */
+ @JvmStatic
+ fun Date.toIsoLocalDate(): String {
+ return LocalDateTime.ofInstant(toInstant(), ZoneId.systemDefault()).toIsoLocalDate()
+ }
+
+ /**
+ * Returns the specified date as an ISO local date string.
+ */
+ @JvmStatic
+ fun LocalDateTime.toIsoLocalDate(): String = format(DateTimeFormatter.ISO_LOCAL_DATE)
+
+ /**
+ * Returns the specified date formatted as `yyyy-MM-dd HH:mm`.
+ */
+ @JvmStatic
+ fun Date.toUtcDateTime(): String {
+ return LocalDateTime.ofInstant(toInstant(), ZoneId.systemDefault()).toUtcDateTime()
+ }
+
+ /**
+ * Returns the specified date formatted as `yyyy-MM-dd HH:mm`.
+ */
+ @JvmStatic
+ fun LocalDateTime.toUtcDateTime(): String = format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))
+
+ /**
+ * Makes the given string bold.
+ */
+ @JvmStatic
+ fun String?.underline(): String = colorize(Colors.UNDERLINE)
+
+
+ /**
+ * Converts XML/XHTML entities to plain text.
+ */
+ @JvmStatic
+ fun String.unescapeXml(): String = Jsoup.parse(this).text()
+
+ /**
+ * Reads contents of a URL.
+ */
+ @JvmStatic
+ @Throws(IOException::class)
+ fun URL.reader(): UrlReaderResponse {
+ val connection = this.openConnection() as HttpURLConnection
+ connection.setRequestProperty(
+ "User-Agent",
+ "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/109.0"
+ )
+ return if (connection.responseCode.isHttpSuccess()) {
+ UrlReaderResponse(connection.responseCode, connection.inputStream.bufferedReader().use { it.readText() })
+ } else {
+ UrlReaderResponse(connection.responseCode, connection.errorStream.bufferedReader().use { it.readText() })
+ }
+ }
+
+ /**
+ * Holds the [URL.reader] response code and body text.
+ */
+ data class UrlReaderResponse(val responseCode: Int, val body: String)
+}
diff --git a/bin/main/net/thauvin/erik/mobibot/commands/AbstractCommand.kt b/bin/main/net/thauvin/erik/mobibot/commands/AbstractCommand.kt
new file mode 100644
index 0000000..5f79472
--- /dev/null
+++ b/bin/main/net/thauvin/erik/mobibot/commands/AbstractCommand.kt
@@ -0,0 +1,79 @@
+/*
+ * AbstractCommand.kt
+ *
+ * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net)
+ *
+ * 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.Utils.bot
+import net.thauvin.erik.mobibot.Utils.helpCmdSyntax
+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
+
+abstract class AbstractCommand {
+ abstract val name: String
+ abstract val help: List
+ abstract val isOpOnly: Boolean
+ abstract val isPublic: Boolean
+ abstract val isVisible: Boolean
+
+ val properties: MutableMap = mutableMapOf()
+
+ abstract fun commandResponse(channel: String, args: String, event: GenericMessageEvent)
+
+ open fun helpResponse(channel: String, topic: String, event: GenericMessageEvent): Boolean {
+ if (!isOpOnly || isOpOnly == event.isChannelOp(channel)) {
+ for (h in help) {
+ event.sendMessage(helpCmdSyntax(h, event.bot().nick, event is PrivateMessageEvent || !isPublic))
+ }
+ return true
+ }
+ return false
+ }
+
+ open fun initProperties(vararg keys: String) {
+ keys.forEach {
+ properties[it] = ""
+ }
+ }
+
+ open fun isEnabled(): Boolean {
+ return true
+ }
+
+ open fun matches(message: String): Boolean {
+ return false
+ }
+
+ open fun setProperty(key: String, value: String) {
+ properties[key] = value
+ }
+}
diff --git a/bin/main/net/thauvin/erik/mobibot/commands/ChannelFeed.kt b/bin/main/net/thauvin/erik/mobibot/commands/ChannelFeed.kt
new file mode 100644
index 0000000..038e378
--- /dev/null
+++ b/bin/main/net/thauvin/erik/mobibot/commands/ChannelFeed.kt
@@ -0,0 +1,62 @@
+/*
+ * ChannelFeed.kt
+ *
+ * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net)
+ *
+ * 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.FeedReader
+import net.thauvin.erik.mobibot.Utils.helpFormat
+import org.pircbotx.hooks.types.GenericMessageEvent
+
+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 isOpOnly = false
+ override val isPublic = true
+ override val isVisible = true
+
+ companion object {
+ const val FEED_PROP = "feed"
+ }
+
+ init {
+ initProperties(FEED_PROP)
+ }
+
+ override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
+ if (isEnabled()) {
+ properties[FEED_PROP]?.let { FeedReader(it, event).run() }
+ }
+ }
+
+ override fun isEnabled(): Boolean {
+ return !properties[FEED_PROP].isNullOrBlank()
+ }
+}
diff --git a/bin/main/net/thauvin/erik/mobibot/commands/Cycle.kt b/bin/main/net/thauvin/erik/mobibot/commands/Cycle.kt
new file mode 100644
index 0000000..9608ca8
--- /dev/null
+++ b/bin/main/net/thauvin/erik/mobibot/commands/Cycle.kt
@@ -0,0 +1,66 @@
+/*
+ * Cycle.kt
+ *
+ * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net)
+ *
+ * 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 kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+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 : 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 isOpOnly = true
+ override val isPublic = false
+ override val isVisible = true
+
+ override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
+ with(event.bot()) {
+ if (event.isChannelOp(channel)) {
+ runBlocking {
+ launch {
+ 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 {
+ helpResponse(channel, args, event)
+ }
+ }
+ }
+}
diff --git a/bin/main/net/thauvin/erik/mobibot/commands/Die.kt b/bin/main/net/thauvin/erik/mobibot/commands/Die.kt
new file mode 100644
index 0000000..f271bfa
--- /dev/null
+++ b/bin/main/net/thauvin/erik/mobibot/commands/Die.kt
@@ -0,0 +1,62 @@
+/*
+ * Die.kt
+ *
+ * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net)
+ *
+ * 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.Utils.bot
+import net.thauvin.erik.mobibot.Utils.isChannelOp
+import org.pircbotx.hooks.types.GenericMessageEvent
+
+class Die : AbstractCommand() {
+ override val name = "die"
+ override val help = emptyList()
+ override val isOpOnly = true
+ override val isPublic = false
+ override val isVisible = false
+
+ override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
+ with(event.bot()) {
+ if (event.isChannelOp(channel) && (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 {
+ const val DIE_PROP = "die"
+ }
+
+ init {
+ initProperties(DIE_PROP)
+ }
+}
diff --git a/bin/main/net/thauvin/erik/mobibot/commands/Ignore.kt b/bin/main/net/thauvin/erik/mobibot/commands/Ignore.kt
new file mode 100644
index 0000000..d083c10
--- /dev/null
+++ b/bin/main/net/thauvin/erik/mobibot/commands/Ignore.kt
@@ -0,0 +1,147 @@
+/*
+ * Ignore.kt
+ *
+ * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net)
+ *
+ * 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.Utils.bold
+import net.thauvin.erik.mobibot.Utils.bot
+import net.thauvin.erik.mobibot.Utils.helpCmdSyntax
+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.LinksManager
+import org.pircbotx.hooks.types.GenericMessageEvent
+
+class Ignore : AbstractCommand() {
+ private val me = "me"
+
+ init {
+ initProperties(IGNORE_PROP)
+ }
+
+ override val name = IGNORE_CMD
+ override val help = listOf(
+ "To ignore a link posted to the channel:",
+ helpFormat("https://www.foo.bar %n"),
+ "To check your ignore status:",
+ helpFormat("%c $name"),
+ "To toggle your ignore status:",
+ helpFormat("%c $name $me")
+ )
+ private val helpOp = help.plus(
+ arrayOf("To add/remove nicks from the ignored list:", helpFormat("%c $name [ ...]"))
+ )
+
+ override val isOpOnly = false
+ override val isPublic = true
+ override val isVisible = true
+
+ companion object {
+ const val IGNORE_CMD = "ignore"
+ const val IGNORE_PROP = IGNORE_CMD
+ private val ignored = mutableSetOf()
+
+ @JvmStatic
+ fun isNotIgnored(nick: String): Boolean {
+ return !ignored.contains(nick.lowercase())
+ }
+ }
+
+ override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
+ val isMe = args.trim().equals(me, true)
+ if (isMe || !event.isChannelOp(channel)) {
+ val nick = event.user.nick.lowercase()
+ ignoreNick(nick, isMe, event)
+ } else {
+ ignoreOp(args, event)
+ }
+ }
+
+ override fun helpResponse(channel: String, topic: String, event: GenericMessageEvent): Boolean {
+ return if (event.isChannelOp(channel)) {
+ for (h in helpOp) {
+ event.sendMessage(helpCmdSyntax(h, event.bot().nick, true))
+ }
+ true
+ } else {
+ super.helpResponse(channel, topic, event)
+ }
+ }
+
+ private fun ignoreNick(sender: String, isMe: Boolean, event: GenericMessageEvent) {
+ if (isMe) {
+ if (ignored.remove(sender)) {
+ event.sendMessage("You are no longer ignored.")
+ } else {
+ ignored.add(sender)
+ event.sendMessage("You are now ignored.")
+ }
+ } else {
+ if (ignored.contains(sender)) {
+ event.sendMessage("You are currently ignored.")
+ } else {
+ event.sendMessage("You are not currently ignored.")
+ }
+ }
+ }
+
+ private fun ignoreOp(args: String, event: GenericMessageEvent) {
+ if (args.isNotEmpty()) {
+ val nicks = args.lowercase().split(" ")
+ for (nick in nicks) {
+ val ignore = if (me == nick) {
+ nick.lowercase()
+ } else {
+ nick
+ }
+ if (!ignored.remove(ignore)) {
+ ignored.add(ignore)
+ }
+ }
+ }
+
+ if (ignored.isNotEmpty()) {
+ event.sendMessage("The following nicks are ignored:")
+ event.sendList(ignored.sorted(), 8, isIndent = true)
+ } else {
+ event.sendMessage("No one is currently ${"ignored".bold()}.")
+ }
+ }
+
+ override fun setProperty(key: String, value: String) {
+ super.setProperty(key, value)
+ if (IGNORE_PROP == key) {
+ ignored.addAll(value.split(LinksManager.TAG_MATCH))
+ }
+ }
+
+}
diff --git a/bin/main/net/thauvin/erik/mobibot/commands/Info.kt b/bin/main/net/thauvin/erik/mobibot/commands/Info.kt
new file mode 100644
index 0000000..ed0b6ef
--- /dev/null
+++ b/bin/main/net/thauvin/erik/mobibot/commands/Info.kt
@@ -0,0 +1,124 @@
+/*
+ * Info.kt
+ *
+ * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net)
+ *
+ * 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.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.plural
+import net.thauvin.erik.mobibot.Utils.sendList
+import net.thauvin.erik.mobibot.Utils.sendMessage
+import net.thauvin.erik.mobibot.commands.links.LinksManager
+import net.thauvin.erik.mobibot.commands.seen.Seen
+import net.thauvin.erik.mobibot.commands.tell.Tell
+import org.pircbotx.hooks.types.GenericMessageEvent
+import java.lang.management.ManagementFactory
+import kotlin.time.DurationUnit
+import kotlin.time.toDuration
+
+class Info(private val tell: Tell, private val seen: Seen) : AbstractCommand() {
+ private val allVersions = listOf(
+ "${ReleaseInfo.PROJECT.capitalise()} ${ReleaseInfo.VERSION} (${ReleaseInfo.WEBSITE.green()})",
+ "Written by ${ReleaseInfo.AUTHOR} (${ReleaseInfo.AUTHOR_URL.green()})"
+ )
+ override val name = "info"
+ override val help = listOf("To view information about the bot:", helpFormat("%c $name"))
+ override val isOpOnly = false
+ override val isPublic = true
+ override val isVisible = true
+
+ companion object {
+ /**
+ * Converts milliseconds to year month week day hour and minutes.
+ */
+ @JvmStatic
+ fun Long.toUptime(): String {
+ this.toDuration(DurationUnit.MILLISECONDS).toComponents { wholeDays, hours, minutes, seconds, _ ->
+ val years = wholeDays / 365
+ var days = wholeDays % 365
+ val months = days / 30
+ days %= 30
+ val weeks = days / 7
+ days %= 7
+
+ with(StringBuffer()) {
+ if (years > 0) {
+ append(years).append(" year".plural(years)).append(' ')
+ }
+ if (months > 0) {
+ append(months).append(" month".plural(months)).append(' ')
+ }
+ if (weeks > 0) {
+ append(weeks).append(" week".plural(weeks)).append(' ')
+ }
+ if (days > 0) {
+ append(days).append(" day".plural(days)).append(' ')
+ }
+ if (hours > 0) {
+ append(hours).append(" hour".plural(hours.toLong())).append(' ')
+ }
+
+ if (minutes > 0) {
+ append(minutes).append(" minute".plural(minutes.toLong()))
+ } else {
+ append(seconds).append(" second".plural(seconds.toLong()))
+ }
+
+ return toString()
+ }
+ }
+ }
+ }
+
+ override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
+ event.sendList(allVersions, 1)
+ val info = StringBuilder()
+ info.append("Uptime: ")
+ .append(ManagementFactory.getRuntimeMXBean().uptime.toUptime())
+ .append(" [Entries: ")
+ .append(LinksManager.entries.links.size)
+ if (seen.isEnabled()) {
+ info.append(", Seen: ").append(seen.count())
+ }
+ if (event.isChannelOp(channel)) {
+ if (tell.isEnabled()) {
+ info.append(", Messages: ").append(tell.size())
+ }
+ if (LinksManager.socialManager.entriesCount() > 0) {
+ info.append(", Social: ").append(LinksManager.socialManager.entriesCount())
+ }
+ }
+ info.append(", Recap: ").append(Recap.recaps.size).append(']')
+ event.sendMessage(info.toString())
+ }
+}
diff --git a/bin/main/net/thauvin/erik/mobibot/commands/Me.kt b/bin/main/net/thauvin/erik/mobibot/commands/Me.kt
new file mode 100644
index 0000000..ec7823b
--- /dev/null
+++ b/bin/main/net/thauvin/erik/mobibot/commands/Me.kt
@@ -0,0 +1,51 @@
+/*
+ * Me.kt
+ *
+ * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net)
+ *
+ * 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.Utils.bot
+import net.thauvin.erik.mobibot.Utils.helpFormat
+import net.thauvin.erik.mobibot.Utils.isChannelOp
+import org.pircbotx.hooks.types.GenericMessageEvent
+
+class Me : AbstractCommand() {
+ override val name = "me"
+ override val help = listOf("To have the bot perform an action:", helpFormat("%c $name "))
+ override val isOpOnly = true
+ override val isPublic = false
+ override val isVisible = true
+
+ override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
+ if (event.isChannelOp(channel)) {
+ event.bot().sendIRC().action(channel, args)
+ }
+ }
+}
diff --git a/bin/main/net/thauvin/erik/mobibot/commands/Modules.kt b/bin/main/net/thauvin/erik/mobibot/commands/Modules.kt
new file mode 100644
index 0000000..b2293b0
--- /dev/null
+++ b/bin/main/net/thauvin/erik/mobibot/commands/Modules.kt
@@ -0,0 +1,63 @@
+/*
+ * Modules.kt
+ *
+ * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net)
+ *
+ * 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.Utils.helpFormat
+import net.thauvin.erik.mobibot.Utils.isChannelOp
+import net.thauvin.erik.mobibot.Utils.sendList
+import org.pircbotx.hooks.types.GenericMessageEvent
+
+class Modules(private val modules: List, private val disabledModules: List) : AbstractCommand() {
+ override val name = "modules"
+ override val help = listOf("To view a list of enabled/disabled modules:", helpFormat("%c $name"))
+ override val isOpOnly = true
+ override val isPublic = false
+ override val isVisible = true
+
+ override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
+ if (event.isChannelOp(channel)) {
+ if (modules.isEmpty()) {
+ event.respondPrivateMessage("There are no enabled modules.")
+ } else {
+ event.respondPrivateMessage("The enabled modules are: ")
+ event.sendList(modules, 7, isIndent = true)
+ }
+ if (disabledModules.isNotEmpty()) {
+ event.respondPrivateMessage("The disabled modules are: ")
+ event.sendList(disabledModules, 7, isIndent = true)
+ }
+ } else {
+ helpResponse(channel, args, event)
+ }
+ }
+}
+
diff --git a/bin/main/net/thauvin/erik/mobibot/commands/Msg.kt b/bin/main/net/thauvin/erik/mobibot/commands/Msg.kt
new file mode 100644
index 0000000..20a6635
--- /dev/null
+++ b/bin/main/net/thauvin/erik/mobibot/commands/Msg.kt
@@ -0,0 +1,60 @@
+/*
+ * Msg.kt
+ *
+ * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net)
+ *
+ * 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.Utils.bot
+import net.thauvin.erik.mobibot.Utils.helpFormat
+import net.thauvin.erik.mobibot.Utils.isChannelOp
+import org.pircbotx.hooks.types.GenericMessageEvent
+
+class Msg : AbstractCommand() {
+ override val name = "msg"
+ override val help = listOf(
+ "To have the bot send a private message to someone:",
+ helpFormat("%c $name ")
+ )
+ override val isOpOnly = true
+ override val isPublic = false
+ override val isVisible = true
+
+ override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
+ if (event.isChannelOp(channel)) {
+ val msg = args.split(" ", limit = 2)
+ if (args.length > 2) {
+ event.bot().sendIRC().message(msg[0], msg[1])
+ event.respondPrivateMessage("A message was sent to ${msg[0]}")
+ } else {
+ helpResponse(channel, args, event)
+ }
+ }
+ }
+}
diff --git a/bin/main/net/thauvin/erik/mobibot/commands/Nick.kt b/bin/main/net/thauvin/erik/mobibot/commands/Nick.kt
new file mode 100644
index 0000000..85a03ab
--- /dev/null
+++ b/bin/main/net/thauvin/erik/mobibot/commands/Nick.kt
@@ -0,0 +1,51 @@
+/*
+ * Nick.kt
+ *
+ * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net)
+ *
+ * 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.Utils.bot
+import net.thauvin.erik.mobibot.Utils.helpFormat
+import net.thauvin.erik.mobibot.Utils.isChannelOp
+import org.pircbotx.hooks.types.GenericMessageEvent
+
+class Nick : AbstractCommand() {
+ override val name = "nick"
+ override val help = listOf("To change the bot's nickname:", helpFormat("%c $name "))
+ override val isOpOnly = true
+ override val isPublic = true
+ override val isVisible = true
+
+ override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
+ if (event.isChannelOp(channel)) {
+ event.bot().sendIRC().changeNick(args)
+ }
+ }
+}
diff --git a/bin/main/net/thauvin/erik/mobibot/commands/Recap.kt b/bin/main/net/thauvin/erik/mobibot/commands/Recap.kt
new file mode 100644
index 0000000..77154c7
--- /dev/null
+++ b/bin/main/net/thauvin/erik/mobibot/commands/Recap.kt
@@ -0,0 +1,81 @@
+/*
+ * Recap.kt
+ *
+ * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net)
+ *
+ * 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.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 : AbstractCommand() {
+ override val name = "recap"
+ override val help = listOf(
+ "To list the last 10 public channel messages:",
+ helpFormat("%c $name")
+ )
+ override val isOpOnly = false
+ override val isPublic = true
+ override val isVisible = true
+
+ companion object {
+ const val MAX_RECAPS = 10
+
+ @JvmField
+ val recaps = mutableListOf()
+
+ /**
+ * Stores the last 10 public messages and actions.
+ */
+ @JvmStatic
+ fun storeRecap(sender: String, message: String, isAction: Boolean) {
+ recaps.add(
+ LocalDateTime.now(Clock.systemUTC()).toUtcDateTime()
+ + " - $sender" + (if (isAction) " " else ": ") + message
+ )
+ if (recaps.size > MAX_RECAPS) {
+ recaps.removeFirst()
+ }
+ }
+ }
+
+ override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
+ if (recaps.isNotEmpty()) {
+ for (r in recaps) {
+ event.sendMessage(r)
+ }
+ } else {
+ event.sendMessage("Sorry, nothing to recap.")
+ }
+ }
+}
diff --git a/bin/main/net/thauvin/erik/mobibot/commands/Say.kt b/bin/main/net/thauvin/erik/mobibot/commands/Say.kt
new file mode 100644
index 0000000..7f76d35
--- /dev/null
+++ b/bin/main/net/thauvin/erik/mobibot/commands/Say.kt
@@ -0,0 +1,51 @@
+/*
+ * Say.kt
+ *
+ * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net)
+ *
+ * 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.Utils.bot
+import net.thauvin.erik.mobibot.Utils.helpFormat
+import net.thauvin.erik.mobibot.Utils.isChannelOp
+import org.pircbotx.hooks.types.GenericMessageEvent
+
+class Say : AbstractCommand() {
+ override val name = "say"
+ override val help = listOf("To have the bot say something on the channel:", helpFormat("%c $name "))
+ override val isOpOnly = true
+ override val isPublic = false
+ override val isVisible = true
+
+ override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
+ if (event.isChannelOp(channel)) {
+ event.bot().sendIRC().message(channel, args)
+ }
+ }
+}
diff --git a/bin/main/net/thauvin/erik/mobibot/commands/Users.kt b/bin/main/net/thauvin/erik/mobibot/commands/Users.kt
new file mode 100644
index 0000000..33d6fef
--- /dev/null
+++ b/bin/main/net/thauvin/erik/mobibot/commands/Users.kt
@@ -0,0 +1,50 @@
+/*
+ * Users.kt
+ *
+ * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net)
+ *
+ * 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.Utils.bot
+import net.thauvin.erik.mobibot.Utils.helpFormat
+import net.thauvin.erik.mobibot.Utils.sendList
+import org.pircbotx.hooks.types.GenericMessageEvent
+
+class Users : AbstractCommand() {
+ override val name = "users"
+ override val help = listOf("To list the users present on the channel:", helpFormat("%c $name"))
+ override val isOpOnly = false
+ override val isPublic = true
+ override val isVisible = true
+
+ override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
+ val ch = event.bot().userChannelDao.getChannel(channel)
+ event.sendList(ch.users.map { if (it.channelsOpIn.contains(ch)) "@${it.nick}" else it.nick }, 8)
+ }
+}
diff --git a/bin/main/net/thauvin/erik/mobibot/commands/Versions.kt b/bin/main/net/thauvin/erik/mobibot/commands/Versions.kt
new file mode 100644
index 0000000..896c569
--- /dev/null
+++ b/bin/main/net/thauvin/erik/mobibot/commands/Versions.kt
@@ -0,0 +1,59 @@
+/*
+ * Versions.kt
+ *
+ * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net)
+ *
+ * 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.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.PircBotX
+import org.pircbotx.hooks.types.GenericMessageEvent
+
+class Versions : AbstractCommand() {
+ private val allVersions = listOf(
+ "Version: ${ReleaseInfo.VERSION} (${ReleaseInfo.BUILDDATE.toIsoLocalDate()})",
+ "${System.getProperty("os.name")} ${System.getProperty("os.version")} (${System.getProperty("os.arch")})" +
+ ", JVM ${System.getProperty("java.runtime.version")}",
+ "Kotlin ${KotlinVersion.CURRENT}, PircBotX ${PircBotX.VERSION}"
+ )
+ override val name = "versions"
+ override val help = listOf("To view the versions data (bot, platform, java, etc.):", helpFormat("%c $name"))
+ override val isOpOnly = true
+ override val isPublic = false
+ override val isVisible = true
+
+ override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
+ if (event.isChannelOp(channel)) {
+ event.sendList(allVersions, 1)
+ }
+ }
+}
diff --git a/bin/main/net/thauvin/erik/mobibot/commands/links/Comment.kt b/bin/main/net/thauvin/erik/mobibot/commands/links/Comment.kt
new file mode 100644
index 0000000..1443d44
--- /dev/null
+++ b/bin/main/net/thauvin/erik/mobibot/commands/links/Comment.kt
@@ -0,0 +1,151 @@
+/*
+ * Comment.kt
+ *
+ * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net)
+ *
+ * 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.links
+
+import net.thauvin.erik.mobibot.Constants
+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.printComment
+import net.thauvin.erik.mobibot.entries.EntriesUtils.toLinkLabel
+import net.thauvin.erik.mobibot.entries.EntryLink
+import org.pircbotx.hooks.types.GenericMessageEvent
+
+class Comment : AbstractCommand() {
+ override val name = COMMAND
+ override val help = listOf(
+ "To add a comment:",
+ helpFormat("${Constants.LINK_CMD}1:This is a comment"),
+ "I will reply with a label, for example: ${Constants.LINK_CMD.bold()}1.1",
+ "To edit a comment, use its label: ",
+ helpFormat("${Constants.LINK_CMD}1.1:This is an edited comment"),
+ "To delete a comment, use its label and a minus sign: ",
+ helpFormat("${Constants.LINK_CMD}1.1:-")
+ )
+ override val isOpOnly = false
+ override val isPublic = true
+ override val isVisible = true
+
+ companion object {
+ const val COMMAND = "comment"
+ }
+
+ override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
+ val cmds = args.substring(1).split("[.:]".toRegex(), 3)
+ val entryIndex = cmds[0].toInt() - 1
+
+ if (entryIndex < LinksManager.entries.links.size && LinksManager.isUpToDate(event)) {
+ val entry: EntryLink = LinksManager.entries.links[entryIndex]
+ val commentIndex = cmds[1].toInt() - 1
+ if (commentIndex < entry.comments.size) {
+ when (val cmd = cmds[2].trim()) {
+ "" -> showComment(entry, entryIndex, commentIndex, event) // L1.1:
+ "-" -> deleteComment(channel, entry, entryIndex, commentIndex, event) // L1.1:-
+ else -> {
+ if (cmd.startsWith('?')) { // L1.1:?
+ changeAuthor(channel, cmd, entry, entryIndex, commentIndex, event)
+ } else { // L1.1:
+ setComment(cmd, entry, entryIndex, commentIndex, event)
+ }
+ }
+ }
+ }
+ }
+ }
+
+ override fun helpResponse(channel: String, topic: String, event: GenericMessageEvent): Boolean {
+ if (super.helpResponse(channel, topic, event)) {
+ if (event.isChannelOp(channel)) {
+ event.sendMessage("To change a comment's author:")
+ event.sendMessage(helpFormat("${Constants.LINK_CMD}1.1:?"))
+ }
+ return true
+ }
+ return false
+ }
+
+ override fun matches(message: String): Boolean {
+ return message.matches("^${Constants.LINK_CMD}\\d+\\.\\d+:.*".toRegex())
+ }
+
+ private fun changeAuthor(
+ channel: String,
+ cmd: String,
+ entry: EntryLink,
+ entryIndex: Int,
+ commentIndex: Int,
+ event: GenericMessageEvent
+ ) {
+ if (event.isChannelOp(channel) && cmd.length > 1) {
+ val comment = entry.getComment(commentIndex)
+ comment.nick = cmd.substring(1)
+ event.sendMessage(printComment(entryIndex, commentIndex, comment))
+ LinksManager.entries.save()
+ } else {
+ event.sendMessage("Please ask a channel op to change the author of this comment for you.")
+ }
+ }
+
+ private fun deleteComment(
+ channel: String,
+ entry: EntryLink,
+ entryIndex: Int,
+ commentIndex: Int,
+ event: GenericMessageEvent
+ ) {
+ if (event.isChannelOp(channel) || event.user.nick == entry.getComment(commentIndex).nick) {
+ entry.deleteComment(commentIndex)
+ event.sendMessage("Comment ${entryIndex.toLinkLabel()}.${commentIndex + 1} removed.")
+ LinksManager.entries.save()
+ } else {
+ event.sendMessage("Please ask a channel op to delete this comment for you.")
+ }
+ }
+
+ private fun setComment(
+ cmd: String,
+ entry: EntryLink,
+ entryIndex: Int,
+ commentIndex: Int,
+ event: GenericMessageEvent
+ ) {
+ entry.setComment(commentIndex, cmd, event.user.nick)
+ event.sendMessage(printComment(entryIndex, commentIndex, entry.getComment(commentIndex)))
+ LinksManager.entries.save()
+ }
+
+ private fun showComment(entry: EntryLink, entryIndex: Int, commentIndex: Int, event: GenericMessageEvent) {
+ event.sendMessage(printComment(entryIndex, commentIndex, entry.getComment(commentIndex)))
+ }
+}
diff --git a/bin/main/net/thauvin/erik/mobibot/commands/links/LinksManager.kt b/bin/main/net/thauvin/erik/mobibot/commands/links/LinksManager.kt
new file mode 100644
index 0000000..fba6b99
--- /dev/null
+++ b/bin/main/net/thauvin/erik/mobibot/commands/links/LinksManager.kt
@@ -0,0 +1,207 @@
+/*
+ * LinksManager.kt
+ *
+ * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net)
+ *
+ * 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.links
+
+import net.thauvin.erik.mobibot.Constants
+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.Companion.isNotIgnored
+import net.thauvin.erik.mobibot.entries.Entries
+import net.thauvin.erik.mobibot.entries.EntriesUtils.printLink
+import net.thauvin.erik.mobibot.entries.EntriesUtils.toLinkLabel
+import net.thauvin.erik.mobibot.entries.EntryLink
+import net.thauvin.erik.mobibot.social.SocialManager
+import org.jsoup.Jsoup
+import org.pircbotx.hooks.types.GenericMessageEvent
+import java.io.IOException
+
+class LinksManager : AbstractCommand() {
+ private val defaultTags: MutableList = mutableListOf()
+ private val keywords: MutableList = mutableListOf()
+
+ override val name = Constants.LINK_CMD
+ override val help = emptyList()
+ override val isOpOnly = false
+ override val isPublic = false
+ override val isVisible = false
+
+ init {
+ initProperties(TAGS_PROP, KEYWORDS_PROP)
+ }
+
+ companion object {
+ val LINK_MATCH = "^[hH][tT][tT][pP](|[sS])://.*".toRegex()
+ const val KEYWORDS_PROP = "tags-keywords"
+ const val TAGS_PROP = "tags"
+ val TAG_MATCH = ", *| +".toRegex()
+
+ /**
+ * Entries array
+ */
+ @JvmField
+ val entries = Entries()
+
+ /**
+ * Pinboard handler.
+ */
+ @JvmField
+ val pinboard = Pinboard()
+
+ /**
+ * Social Manager handler.
+ */
+ @JvmField
+ val socialManager = SocialManager()
+
+ /**
+ * Let the user know if the entries are too old to be modified.
+ */
+ @JvmStatic
+ fun isUpToDate(event: GenericMessageEvent): Boolean {
+ if (entries.lastPubDate != today()) {
+ event.sendMessage("The links are too old to be updated.")
+ return false
+ }
+ return true
+ }
+ }
+
+ 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 (isNotIgnored(sender) && (cmds.size == 1 || !cmds[1].contains(botNick))) {
+ val link = cmds[0].trim()
+ if (!isDupEntry(link, event)) {
+ var title = ""
+ val tags = ArrayList(defaultTags)
+ if (cmds.size == 2) {
+ val data = cmds[1].trim().split("${Tags.COMMAND}:", limit = 2)
+ title = data[0].trim()
+ if (data.size > 1) {
+ tags.addAll(data[1].split(TAG_MATCH))
+ }
+ }
+
+ if (title.isBlank()) {
+ title = fetchTitle(link)
+ }
+
+ if (title != Constants.NO_TITLE) {
+ matchTagKeywords(title, tags)
+ }
+
+ // Links are old, clear them
+ if (entries.lastPubDate != today()) {
+ entries.links.clear()
+ }
+
+ val entry = EntryLink(link, title, sender, login, channel, tags)
+ entries.links.add(entry)
+ val index = entries.links.lastIndexOf(entry)
+ event.sendMessage(printLink(index, entry))
+
+ pinboard.addPin(event.bot().serverHostname, entry)
+
+ // Queue link for posting to social media.
+ socialManager.queueEntry(index)
+
+ entries.save()
+
+ if (Constants.NO_TITLE == entry.title) {
+ event.sendMessage("Please specify a title, by typing:")
+ event.sendMessage(helpFormat("${index.toLinkLabel()}:|This is the title"))
+ }
+ }
+ }
+ }
+
+ override fun helpResponse(channel: String, topic: String, event: GenericMessageEvent): Boolean = false
+
+ override fun matches(message: String): Boolean {
+ return message.matches(LINK_MATCH)
+ }
+
+ internal fun fetchTitle(link: String): String {
+ try {
+ val html = Jsoup.connect(link)
+ .userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:74.0) Gecko/20100101 Firefox/74.0")
+ .get()
+ val title = html.title()
+ if (title.isNotBlank()) {
+ return title
+ }
+ } catch (ignore: IOException) {
+ // Do nothing
+ }
+ return Constants.NO_TITLE
+ }
+
+ private fun isDupEntry(link: String, event: GenericMessageEvent): Boolean {
+ synchronized(entries) {
+ return try {
+ val match = entries.links.single { it.link == link }
+ event.sendMessage(
+ "Duplicate".bold() + " >> " + printLink(entries.links.indexOf(match), match)
+ )
+ true
+ } catch (ignore: NoSuchElementException) {
+ false
+ }
+ }
+ }
+
+ internal fun matchTagKeywords(title: String, tags: MutableList) {
+ for (match in keywords) {
+ val m = Regex.escape(match)
+ if (title.matches("(?i).*\\b$m\\b.*".toRegex())) {
+ tags.add(match)
+ }
+ }
+ }
+
+ override fun setProperty(key: String, value: String) {
+ super.setProperty(key, value)
+ if (KEYWORDS_PROP == key) {
+ keywords.addAll(value.split(TAG_MATCH))
+ } else if (TAGS_PROP == key) {
+ defaultTags.addAll(value.split(TAG_MATCH))
+ }
+ }
+}
diff --git a/bin/main/net/thauvin/erik/mobibot/commands/links/Posting.kt b/bin/main/net/thauvin/erik/mobibot/commands/links/Posting.kt
new file mode 100644
index 0000000..ff4278d
--- /dev/null
+++ b/bin/main/net/thauvin/erik/mobibot/commands/links/Posting.kt
@@ -0,0 +1,164 @@
+/*
+ * Posting.kt
+ *
+ * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net)
+ *
+ * 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.links
+
+import net.thauvin.erik.mobibot.Constants
+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.LinksManager.Companion.entries
+import net.thauvin.erik.mobibot.entries.EntriesUtils
+import net.thauvin.erik.mobibot.entries.EntriesUtils.toLinkLabel
+import net.thauvin.erik.mobibot.entries.EntryLink
+import org.pircbotx.hooks.types.GenericMessageEvent
+
+class Posting : AbstractCommand() {
+ override val name = "posting"
+ override val help = listOf(
+ "Post a URL, by saying it on a line on its own:",
+ helpFormat(" [] ${Tags.COMMAND}: <+tag> [...]]"),
+ "I will reply with a label, for example: ${Constants.LINK_CMD.bold()}1",
+ "To add a title, use its label and a pipe:",
+ helpFormat("${Constants.LINK_CMD}1:|This is the title"),
+ "To add a comment:",
+ helpFormat("${Constants.LINK_CMD}1:This is a comment"),
+ "I will reply with a label, for example: ${Constants.LINK_CMD.bold()}1.1",
+ "To edit a comment, see: ",
+ helpFormat("%c ${Constants.HELP_CMD} ${Comment.COMMAND}")
+ )
+ override val isOpOnly = false
+ override val isPublic = true
+ override val isVisible = true
+
+ override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
+ val cmds = args.substring(1).split(":", limit = 2)
+ val entryIndex = cmds[0].toInt() - 1
+
+ if (entryIndex < entries.links.size) {
+ val cmd = cmds[1].trim()
+ if (cmd.isBlank()) {
+ showEntry(entryIndex, event) // L1:
+ } else if (LinksManager.isUpToDate(event)) {
+ if (cmd == "-") {
+ removeEntry(channel, entryIndex, event) // L1:-
+ } else {
+ when (cmd[0]) {
+ '|' -> changeTitle(cmd, entryIndex, event) // L1:|
+ '=' -> changeUrl(channel, cmd, entryIndex, event) // L1:=
+ '?' -> changeAuthor(channel, cmd, entryIndex, event) // L1:?
+ else -> addComment(cmd, entryIndex, event) // L1:
+ }
+ }
+ }
+ }
+ }
+
+ override fun matches(message: String): Boolean {
+ return message.matches("${Constants.LINK_CMD}\\d+:.*".toRegex())
+ }
+
+ 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)
+ event.sendMessage(EntriesUtils.printComment(entryIndex, commentIndex, comment))
+ entries.save()
+ }
+
+ private fun changeTitle(cmd: String, entryIndex: Int, event: GenericMessageEvent) {
+ if (cmd.length > 1) {
+ val entry: EntryLink = entries.links[entryIndex]
+ entry.title = cmd.substring(1).trim()
+ LinksManager.pinboard.updatePin(event.bot().serverHostname, entry.link, entry)
+ event.sendMessage(EntriesUtils.printLink(entryIndex, entry))
+ entries.save()
+ }
+ }
+
+ private fun changeUrl(channel: String, cmd: String, entryIndex: Int, event: GenericMessageEvent) {
+ val entry: EntryLink = entries.links[entryIndex]
+ if (entry.login == event.user.login || event.isChannelOp(channel)) {
+ val link = cmd.substring(1)
+ if (link.matches(LinksManager.LINK_MATCH)) {
+ val oldLink = entry.link
+ entry.link = link
+ LinksManager.pinboard.updatePin(event.bot().serverHostname, oldLink, entry)
+ event.sendMessage(EntriesUtils.printLink(entryIndex, entry))
+ entries.save()
+ }
+ }
+ }
+
+ private fun changeAuthor(channel: String, cmd: String, index: Int, event: GenericMessageEvent) {
+ if (event.isChannelOp(channel)) {
+ if (cmd.length > 1) {
+ val entry: EntryLink = entries.links[index]
+ entry.nick = cmd.substring(1)
+ LinksManager.pinboard.updatePin(event.bot().serverHostname, entry.link, entry)
+ event.sendMessage(EntriesUtils.printLink(index, entry))
+ entries.save()
+ }
+ } else {
+ event.sendMessage("Please ask a channel op to change the author of this link for you.")
+ }
+ }
+
+ private fun removeEntry(channel: String, index: Int, event: GenericMessageEvent) {
+ val entry: EntryLink = entries.links[index]
+ if (entry.login == event.user.login || event.isChannelOp(channel)) {
+ LinksManager.pinboard.deletePin(entry)
+ LinksManager.socialManager.removeEntry(index)
+ entries.links.removeAt(index)
+ event.sendMessage("Entry ${index.toLinkLabel()} removed.")
+ entries.save()
+ } else {
+ event.sendMessage("Please ask a channel op to remove this entry for you.")
+ }
+ }
+
+ private fun showEntry(index: Int, event: GenericMessageEvent) {
+ val entry: EntryLink = entries.links[index]
+ event.sendMessage(EntriesUtils.printLink(index, entry))
+ if (entry.tags.isNotEmpty()) {
+ event.sendMessage(EntriesUtils.printTags(index, entry))
+ }
+ if (entry.comments.isNotEmpty()) {
+ val comments = entry.comments
+ for (i in comments.indices) {
+ event.sendMessage(EntriesUtils.printComment(index, i, comments[i]))
+ }
+ }
+ }
+}
diff --git a/bin/main/net/thauvin/erik/mobibot/commands/links/Tags.kt b/bin/main/net/thauvin/erik/mobibot/commands/links/Tags.kt
new file mode 100644
index 0000000..1662857
--- /dev/null
+++ b/bin/main/net/thauvin/erik/mobibot/commands/links/Tags.kt
@@ -0,0 +1,87 @@
+/*
+ * Tags.kt
+ *
+ * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net)
+ *
+ * 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.links
+
+import net.thauvin.erik.mobibot.Constants
+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 : 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 isOpOnly = false
+ override val isPublic = true
+ override val isVisible = true
+
+ companion object {
+ const val COMMAND = "tags"
+ }
+
+ override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
+ val cmds = args.substring(1).split("${Constants.TAG_CMD}:", limit = 2)
+ val index = cmds[0].toInt() - 1
+
+ if (index < LinksManager.entries.links.size && LinksManager.isUpToDate(event)) {
+ val cmd = cmds[1].trim()
+ val entry: EntryLink = LinksManager.entries.links[index]
+ if (cmd.isNotEmpty()) {
+ if (entry.login == event.user.login || event.isChannelOp(channel)) {
+ entry.setTags(cmd)
+ LinksManager.pinboard.updatePin(event.bot().serverHostname, entry.link, entry)
+ event.sendMessage(EntriesUtils.printTags(index, entry))
+ LinksManager.entries.save()
+ } else {
+ event.sendMessage("Please ask a channel op to change the tags for you.")
+ }
+ } else {
+ if (entry.tags.isNotEmpty()) {
+ event.sendMessage(EntriesUtils.printTags(index, entry))
+ } else {
+ event.sendMessage("The entry has no tags. Why don't add some?")
+ }
+ }
+ }
+ }
+
+ override fun matches(message: String): Boolean {
+ return message.matches("^${Constants.LINK_CMD}\\d+${Constants.TAG_CMD}:.*".toRegex())
+ }
+}
diff --git a/bin/main/net/thauvin/erik/mobibot/commands/links/View.kt b/bin/main/net/thauvin/erik/mobibot/commands/links/View.kt
new file mode 100644
index 0000000..825e374
--- /dev/null
+++ b/bin/main/net/thauvin/erik/mobibot/commands/links/View.kt
@@ -0,0 +1,120 @@
+/*
+ * View.kt
+ *
+ * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net)
+ *
+ * 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.links
+
+import net.thauvin.erik.mobibot.Utils.bot
+import net.thauvin.erik.mobibot.Utils.helpCmdSyntax
+import net.thauvin.erik.mobibot.Utils.helpFormat
+import net.thauvin.erik.mobibot.Utils.lastOrEmpty
+import net.thauvin.erik.mobibot.Utils.sendMessage
+import net.thauvin.erik.mobibot.commands.AbstractCommand
+import net.thauvin.erik.mobibot.commands.links.LinksManager.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 : AbstractCommand() {
+ override val name = VIEW_CMD
+ override val help = listOf(
+ "To list or search the current URL posts:",
+ helpFormat("%c $name [] []")
+ )
+ override val isOpOnly = false
+ override val isPublic = true
+ override val isVisible = true
+
+ companion object {
+ const val MAX_ENTRIES = 6
+ const val VIEW_CMD = "view"
+ }
+
+ override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
+ if (entries.links.isNotEmpty()) {
+ val p = parseArgs(args)
+ showPosts(p.first, p.second, event)
+ } else {
+ event.sendMessage("There is currently nothing to view. Why don't you post something?")
+ }
+ }
+
+ internal fun parseArgs(args: String): Pair {
+ var query = args.lowercase().trim()
+ var start = 0
+ if (query.isEmpty() && entries.links.size > MAX_ENTRIES) {
+ start = entries.links.size - MAX_ENTRIES
+ }
+ if (query.matches("^\\d+(| .*)".toRegex())) { // view [] []
+ val split = query.split(" ", limit = 2)
+ try {
+ start = split[0].toInt() - 1
+ query = split.lastOrEmpty().trim()
+ if (start > entries.links.size) {
+ start = 0
+ }
+ } catch (ignore: NumberFormatException) {
+ // Do nothing
+ }
+ }
+ return Pair(start, query)
+ }
+
+ private fun showPosts(start: Int, query: String, event: GenericMessageEvent) {
+ var index = start
+ var entry: EntryLink
+ var sent = 0
+ while (index < entries.links.size && sent < MAX_ENTRIES) {
+ entry = entries.links[index]
+ if (query.isNotBlank()) {
+ if (entry.matches(query)) {
+ event.sendMessage(EntriesUtils.printLink(index, entry, true))
+ sent++
+ }
+ } else {
+ event.sendMessage(EntriesUtils.printLink(index, entry, true))
+ sent++
+ }
+ index++
+ if (sent == MAX_ENTRIES && index < entries.links.size) {
+ event.sendMessage("To view more, try: ")
+ event.sendMessage(
+ helpFormat(
+ helpCmdSyntax("%c $name ${index + 1} $query", event.bot().nick, event is PrivateMessageEvent)
+ )
+ )
+ }
+ }
+ if (sent == 0) {
+ event.sendMessage("No matches. Please try again.")
+ }
+ }
+}
diff --git a/bin/main/net/thauvin/erik/mobibot/commands/seen/NickComparator.kt b/bin/main/net/thauvin/erik/mobibot/commands/seen/NickComparator.kt
new file mode 100644
index 0000000..cfd2c27
--- /dev/null
+++ b/bin/main/net/thauvin/erik/mobibot/commands/seen/NickComparator.kt
@@ -0,0 +1,45 @@
+/*
+ * NickComparator.kt
+ *
+ * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net)
+ *
+ * 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.seen
+
+import java.io.Serializable
+
+class NickComparator : Comparator, Serializable {
+ override fun compare(a: String, b: String): Int {
+ return a.lowercase().compareTo(b.lowercase())
+ }
+
+ companion object {
+ @Suppress("ConstPropertyName")
+ private const val serialVersionUID = 1L
+ }
+}
diff --git a/bin/main/net/thauvin/erik/mobibot/commands/seen/Seen.kt b/bin/main/net/thauvin/erik/mobibot/commands/seen/Seen.kt
new file mode 100644
index 0000000..c9ee0f3
--- /dev/null
+++ b/bin/main/net/thauvin/erik/mobibot/commands/seen/Seen.kt
@@ -0,0 +1,150 @@
+/*
+ * Seen.kt
+ *
+ * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net)
+ *
+ * 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.seen
+
+import com.google.common.collect.ImmutableSortedSet
+import net.thauvin.erik.mobibot.Utils
+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.loadSerialData
+import net.thauvin.erik.mobibot.Utils.saveSerialData
+import net.thauvin.erik.mobibot.Utils.sendList
+import net.thauvin.erik.mobibot.Utils.sendMessage
+import net.thauvin.erik.mobibot.commands.AbstractCommand
+import net.thauvin.erik.mobibot.commands.Info.Companion.toUptime
+import org.pircbotx.User
+import org.pircbotx.hooks.types.GenericMessageEvent
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+import java.util.*
+
+
+class Seen(private val serialObject: String) : AbstractCommand() {
+ private val logger: Logger = LoggerFactory.getLogger(Seen::class.java)
+ private val allKeyword = "all"
+ val seenNicks = TreeMap(NickComparator())
+
+ override val name = "seen"
+ override val help = listOf("To view when a nickname was last seen:", helpFormat("%c $name "))
+ private val helpOp = help.plus(
+ arrayOf("To view all ${"seen".bold()} nicks:", helpFormat("%c $name $allKeyword"))
+ )
+ override val isOpOnly = false
+ override val isPublic = true
+ override val isVisible = true
+
+
+ override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
+ if (isEnabled()) {
+ if (args.isNotBlank() && !args.contains(' ')) {
+ val ch = event.bot().userChannelDao.getChannel(channel)
+ if (args == allKeyword && ch.isOp(event.user) && seenNicks.isNotEmpty()) {
+ event.sendMessage("The ${"seen".bold()} nicks are:")
+ event.sendList(seenNicks.keys.toList(), 7, separator = ", ", isIndent = true)
+ return
+ }
+ ch.users.forEach {
+ if (args.equals(it.nick, true)) {
+ event.sendMessage("${it.nick} is on ${channel}.")
+ return
+ }
+ }
+ if (seenNicks.containsKey(args)) {
+ val seenNick = seenNicks.getValue(args)
+ val lastSeen = System.currentTimeMillis() - seenNick.lastSeen
+ event.sendMessage("${seenNick.nick} was last seen on $channel ${lastSeen.toUptime()} ago.")
+ return
+ }
+ event.sendMessage("I haven't seen $args on $channel lately.")
+ } else {
+ helpResponse(channel, args, event)
+ }
+ }
+ }
+
+ fun add(nick: String) {
+ if (isEnabled()) {
+ seenNicks[nick] = SeenNick(nick, System.currentTimeMillis())
+ save()
+ }
+ }
+
+ fun add(users: ImmutableSortedSet) {
+ if (isEnabled()) {
+ users.forEach {
+ seenNicks[it.nick] = SeenNick(it.nick, System.currentTimeMillis())
+ }
+ save()
+ }
+ }
+
+ fun clear() {
+ seenNicks.clear()
+ }
+
+ fun count(): Int = seenNicks.size
+
+ override fun helpResponse(channel: String, topic: String, event: GenericMessageEvent): Boolean {
+ return if (event.isChannelOp(channel)) {
+ for (h in helpOp) {
+ event.sendMessage(Utils.helpCmdSyntax(h, event.bot().nick, true))
+ }
+ true
+ } else {
+ super.helpResponse(channel, topic, event)
+ }
+ }
+
+ fun load() {
+ if (isEnabled()) {
+ @Suppress("UNCHECKED_CAST")
+ seenNicks.putAll(
+ loadSerialData(
+ serialObject,
+ TreeMap(),
+ logger,
+ "seen nicknames"
+ ) as TreeMap
+ )
+ }
+ }
+
+ fun save() {
+ saveSerialData(serialObject, seenNicks, logger, "seen nicknames")
+ }
+
+ init {
+ load()
+ }
+}
diff --git a/bin/main/net/thauvin/erik/mobibot/commands/seen/SeenNick.kt b/bin/main/net/thauvin/erik/mobibot/commands/seen/SeenNick.kt
new file mode 100644
index 0000000..7924977
--- /dev/null
+++ b/bin/main/net/thauvin/erik/mobibot/commands/seen/SeenNick.kt
@@ -0,0 +1,41 @@
+/*
+ * SeenNick.kt
+ *
+ * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net)
+ *
+ * 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.seen
+
+import java.io.Serializable
+
+data class SeenNick(val nick: String, val lastSeen: Long) : Serializable {
+ companion object {
+ @Suppress("ConstPropertyName")
+ private const val serialVersionUID = 1L
+ }
+}
diff --git a/bin/main/net/thauvin/erik/mobibot/commands/tell/Tell.kt b/bin/main/net/thauvin/erik/mobibot/commands/tell/Tell.kt
new file mode 100644
index 0000000..061ca6a
--- /dev/null
+++ b/bin/main/net/thauvin/erik/mobibot/commands/tell/Tell.kt
@@ -0,0 +1,306 @@
+/*
+ * Tell.kt
+ *
+ * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net)
+ *
+ * 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.tell
+
+import net.thauvin.erik.mobibot.Utils.bold
+import net.thauvin.erik.mobibot.Utils.bot
+import net.thauvin.erik.mobibot.Utils.helpCmdSyntax
+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(private val serialObject: String) : AbstractCommand() {
+ // Messages queue
+ private val messages: MutableList = mutableListOf()
+
+ // Maximum number of days to keep messages
+ private var maxDays = 7
+
+ // Message maximum queue size
+ private var maxSize = 50
+
+ /**
+ * The tell command.
+ */
+ override val name = "tell"
+
+ override val help = listOf(
+ "To send a message to someone when they join the channel:",
+ helpFormat("%c $name "),
+ "To view queued and sent messages:",
+ helpFormat("%c $name ${View.VIEW_CMD}"),
+ "Messages are kept for ${maxDays.bold()}" + " day".plural(maxDays.toLong()) + '.'
+ )
+ override val isOpOnly: Boolean = false
+ override val isPublic: Boolean = isEnabled()
+ override val isVisible: Boolean = isEnabled()
+
+ /**
+ * Cleans the messages queue.
+ */
+ private fun clean(): Boolean {
+ return TellManager.clean(messages, maxDays.toLong())
+ }
+
+ override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
+ if (isEnabled()) {
+ when {
+ args.isBlank() -> {
+ helpResponse(channel, args, event)
+ }
+
+ args.startsWith(View.VIEW_CMD) -> {
+ if (event.isChannelOp(channel) && "${View.VIEW_CMD} $TELL_ALL_KEYWORD" == args) {
+ viewAll(event)
+ } else {
+ viewMessages(event)
+ }
+ }
+
+ args.startsWith("$TELL_DEL_KEYWORD ") -> {
+ deleteMessage(channel, args, event)
+ }
+
+ else -> {
+ newMessage(channel, args, event)
+ }
+ }
+ if (clean()) {
+ save()
+ }
+ }
+ }
+
+ // Delete message.
+ private fun deleteMessage(channel: String, args: String, event: GenericMessageEvent) {
+ val split = args.split(" ")
+ if (split.size == 2) {
+ val id = split[1]
+ if (TELL_ALL_KEYWORD.equals(id, ignoreCase = true)) {
+ if (messages.removeIf { it.sender.equals(event.user.nick, true) && it.isReceived }) {
+ save()
+ event.sendMessage("Delivered messages have been deleted.")
+ } else {
+ event.sendMessage("No delivered messages were found.")
+ }
+ } else {
+ if (messages.removeIf {
+ it.id == id &&
+ (it.sender.equals(event.user.nick, true) || event.isChannelOp(channel))
+ }) {
+ save()
+ event.sendMessage("The message was deleted from the queue.")
+ } else {
+ event.sendMessage("The specified message [ID $id] could not be found.")
+ }
+ }
+ } else {
+ helpResponse(channel, args, event)
+ }
+ }
+
+ override fun isEnabled(): Boolean {
+ return maxSize > 0 && maxDays > 0
+ }
+
+ override fun setProperty(key: String, value: String) {
+ super.setProperty(key, value)
+ if (MAX_DAYS_PROP == key) {
+ maxDays = value.toIntOrDefault(maxDays)
+ } else if (MAX_SIZE_PROP == key) {
+ maxSize = value.toIntOrDefault(maxSize)
+ }
+ }
+
+ // New message.
+ 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(event.user.nick, split[0], split[1].trim())
+ messages.add(message)
+ save()
+ event.sendMessage("Message [ID ${message.id}] was queued for ${message.recipient.bold()}")
+ } else {
+ event.sendMessage("Sorry, the messages queue is currently full.")
+ }
+ } else {
+ helpResponse(channel, args, event)
+ }
+ }
+
+ /**
+ * Saves the messages queue.
+ */
+ private fun save() {
+ TellManager.save(serialObject, messages)
+ }
+
+ /**
+ * Checks and sends messages.
+ */
+ fun send(event: GenericUserEvent) {
+ val nickname = event.user.nick
+ if (isEnabled() && nickname != event.getBot().nick) {
+ messages.filter { it.isMatch(nickname) }.forEach { message ->
+ if (message.recipient.equals(nickname, ignoreCase = true) && !message.isReceived) {
+ if (message.sender == nickname) {
+ if (event !is MessageEvent) {
+ event.user.send().message(
+ "${"You".bold()} wanted me to remind you: ${message.message.reverseColor()}"
+ )
+ message.isReceived = true
+ message.isNotified = true
+ save()
+ }
+ } else {
+ event.user.send().message(
+ "${message.sender} wanted me to tell you: ${message.message.reverseColor()}"
+ )
+ message.isReceived = true
+ save()
+ }
+ } else if (message.sender.equals(nickname, ignoreCase = true) && message.isReceived
+ && !message.isNotified
+ ) {
+ event.user.send().message(
+ "Your message ${"[ID ${message.id}]".reverseColor()} was sent to "
+ + "${message.recipient.bold()} on ${message.receptionDate}"
+ )
+ message.isNotified = true
+ save()
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns the messages queue size.
+ *
+ * @return The size.
+ */
+ fun size(): Int = messages.size
+
+ // View all messages.
+ private fun viewAll(event: GenericMessageEvent) {
+ if (messages.isNotEmpty()) {
+ for (message in messages) {
+ event.sendMessage(
+ "${message.sender.bold()}$ARROW${message.recipient.bold()} [ID: ${message.id}, " +
+ (if (message.isReceived) "DELIVERED]" else "QUEUED]")
+ )
+ }
+ } else {
+ event.sendMessage("There are no messages in the queue.")
+ }
+ }
+
+ // View messages.
+ private fun viewMessages(event: GenericMessageEvent) {
+ var hasMessage = false
+ for (message in messages.filter { it.isMatch(event.user.nick) }) {
+ if (!hasMessage) {
+ hasMessage = true
+ event.sendMessage("Here are your messages: ")
+ }
+ if (message.isReceived) {
+ event.sendMessage(
+ message.sender.bold() + ARROW + message.recipient.bold() +
+ " [${message.receptionDate.toUtcDateTime()}, ID: ${message.id.bold()}, DELIVERED]"
+ )
+ } else {
+ event.sendMessage(
+ message.sender.bold() + ARROW + message.recipient.bold() +
+ " [${message.queued.toUtcDateTime()}, ID: ${message.id.bold()}, QUEUED]"
+ )
+ }
+ event.sendMessage(helpFormat(message.message))
+ }
+ if (!hasMessage) {
+ event.sendMessage("You have no messages in the queue.")
+ } else {
+ event.sendMessage("To delete one or all delivered messages:")
+ event.sendMessage(
+ helpFormat(
+ helpCmdSyntax("%c $name $TELL_DEL_KEYWORD ", event.bot().nick, true)
+ )
+ )
+ event.sendMessage(help.last())
+ }
+ }
+
+ companion object {
+ /**
+ * Max days property.
+ */
+ const val MAX_DAYS_PROP = "tell-max-days"
+
+ /**
+ * Max size property.
+ */
+ const val MAX_SIZE_PROP = "tell-max-size"
+
+ // Arrow
+ private const val ARROW = " --> "
+
+ // All keyword
+ private const val TELL_ALL_KEYWORD = "all"
+
+ //T he delete command.
+ private const val TELL_DEL_KEYWORD = "del"
+ }
+
+ /**
+ * Creates a new instance.
+ */
+ init {
+ initProperties(MAX_DAYS_PROP, MAX_SIZE_PROP)
+
+ // Load the message queue
+ messages.addAll(TellManager.load(serialObject))
+ if (clean()) {
+ save()
+ }
+ }
+}
diff --git a/bin/main/net/thauvin/erik/mobibot/commands/tell/TellManager.kt b/bin/main/net/thauvin/erik/mobibot/commands/tell/TellManager.kt
new file mode 100644
index 0000000..b65a4da
--- /dev/null
+++ b/bin/main/net/thauvin/erik/mobibot/commands/tell/TellManager.kt
@@ -0,0 +1,74 @@
+/*
+ * TellManager.kt
+ *
+ * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net)
+ *
+ * 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.tell
+
+import net.thauvin.erik.mobibot.Utils.loadSerialData
+import net.thauvin.erik.mobibot.Utils.saveSerialData
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+import java.time.Clock
+import java.time.LocalDateTime
+
+/**
+ * The Tell Messages Manager.
+ */
+object TellManager {
+ private val logger: Logger = LoggerFactory.getLogger(TellManager::class.java)
+
+ /**
+ * Cleans the messages queue.
+ */
+ @JvmStatic
+ fun clean(tellMessages: MutableList, tellMaxDays: Long): Boolean {
+ if (logger.isDebugEnabled) logger.debug("Cleaning the messages.")
+ val today = LocalDateTime.now(Clock.systemUTC())
+ return tellMessages.removeIf { o: TellMessage -> o.queued.plusDays(tellMaxDays).isBefore(today) }
+ }
+
+ /**
+ * Loads the messages.
+ */
+ @JvmStatic
+ fun load(file: String): List {
+ @Suppress("UNCHECKED_CAST")
+ return loadSerialData(file, emptyList(), logger, "message queue") as List
+ }
+
+ /**
+ * Saves the messages.
+ */
+ @JvmStatic
+ fun save(file: String, messages: List?) {
+ if (messages != null) {
+ saveSerialData(file, messages, logger, "messages")
+ }
+ }
+}
diff --git a/bin/main/net/thauvin/erik/mobibot/commands/tell/TellMessage.kt b/bin/main/net/thauvin/erik/mobibot/commands/tell/TellMessage.kt
new file mode 100644
index 0000000..d17fbb5
--- /dev/null
+++ b/bin/main/net/thauvin/erik/mobibot/commands/tell/TellMessage.kt
@@ -0,0 +1,104 @@
+/*
+ * TellMessage.kt
+ *
+ * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net)
+ *
+ * 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.tell
+
+import java.io.Serializable
+import java.time.Clock
+import java.time.LocalDateTime
+import java.time.format.DateTimeFormatter
+
+/**
+ * Tell Message.
+ */
+class TellMessage(
+ /**
+ * Returns the message's sender.
+ */
+ val sender: String,
+
+ /**
+ * Returns the message's recipient.
+ */
+ val recipient: String,
+
+ /**
+ * Returns the message text.
+ */
+ val message: String
+) : Serializable {
+ /**
+ * Returns the queued date/time.
+ */
+ var queued: LocalDateTime = LocalDateTime.now(Clock.systemUTC())
+
+ /**
+ * Returns the message id.
+ */
+ var id: String = queued.format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"))
+
+ /**
+ * Returns `true` if a notification was sent.
+ */
+ var isNotified = false
+
+ /**
+ * Returns `true` if the message was received.
+ */
+ var isReceived = false
+ set(value) {
+ if (value) {
+ receptionDate = LocalDateTime.now(Clock.systemUTC())
+ }
+ field = value
+ }
+
+ /**
+ * Returns the message creating date.
+ */
+ var receptionDate: LocalDateTime = LocalDateTime.MIN
+
+ /**
+ * Matches the message sender or recipient.
+ */
+ fun isMatch(nick: String?): Boolean {
+ return sender.equals(nick, ignoreCase = true) || recipient.equals(nick, ignoreCase = true)
+ }
+
+ override fun toString(): String {
+ return ("TellMessage{id='$id', isNotified=$isNotified, isReceived=$isReceived, message='$message', " +
+ "queued=$queued, received=$receptionDate, recipient='$recipient', sender='$sender'}")
+ }
+
+ companion object {
+ @Suppress("ConstPropertyName")
+ private const val serialVersionUID = 2L
+ }
+}
diff --git a/bin/main/net/thauvin/erik/mobibot/entries/Entries.kt b/bin/main/net/thauvin/erik/mobibot/entries/Entries.kt
new file mode 100644
index 0000000..e8676ec
--- /dev/null
+++ b/bin/main/net/thauvin/erik/mobibot/entries/Entries.kt
@@ -0,0 +1,54 @@
+/*
+ * Entries.kt
+ *
+ * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net)
+ *
+ * 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 net.thauvin.erik.mobibot.Utils.today
+
+class Entries(
+ var channel: String = "",
+ var ircServer: String = "",
+ var logsDir: String = "",
+ var backlogs: String = ""
+) {
+ val links = mutableListOf()
+
+ var lastPubDate = today()
+
+ fun load() {
+ lastPubDate = FeedsManager.loadFeed(this)
+ }
+
+ fun save() {
+ lastPubDate = today()
+ FeedsManager.saveFeed(this)
+ }
+}
diff --git a/bin/main/net/thauvin/erik/mobibot/entries/EntriesUtils.kt b/bin/main/net/thauvin/erik/mobibot/entries/EntriesUtils.kt
new file mode 100644
index 0000000..9c09626
--- /dev/null
+++ b/bin/main/net/thauvin/erik/mobibot/entries/EntriesUtils.kt
@@ -0,0 +1,83 @@
+/*
+ * EntriesUtils.kt
+ *
+ * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net)
+ *
+ * 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 net.thauvin.erik.mobibot.Constants
+import net.thauvin.erik.mobibot.Utils.bold
+import net.thauvin.erik.mobibot.Utils.green
+
+/**
+ * Entries utilities.
+ */
+object EntriesUtils {
+ /**
+ * Prints an entry's comment for display on the channel.
+ */
+ @JvmStatic
+ fun printComment(entryIndex: Int, commentIndex: Int, comment: EntryComment): String =
+ ("${entryIndex.toLinkLabel()}.${commentIndex + 1}: [${comment.nick}] ${comment.comment}")
+
+ /**
+ * Prints an entry's link for display on the channel.
+ */
+ @JvmStatic
+ @JvmOverloads
+ fun printLink(entryIndex: Int, entry: EntryLink, isView: Boolean = false): String {
+ val buff = StringBuilder().append(entryIndex.toLinkLabel()).append(": ")
+ .append('[').append(entry.nick).append(']')
+ if (isView && entry.comments.isNotEmpty()) {
+ buff.append("[+").append(entry.comments.size).append(']')
+ }
+ buff.append(' ')
+ with(entry) {
+ if (Constants.NO_TITLE == title) {
+ buff.append(title)
+ } else {
+ buff.append(title.bold())
+ }
+ buff.append(" ( ").append(link.green()).append(" )")
+ }
+ return buff.toString()
+ }
+
+ /**
+ * Prints an entry's tags/categories for display on the channel. e.g. L1T: tag1, tag2
+ */
+ @JvmStatic
+ fun printTags(entryIndex: Int, entry: EntryLink): String =
+ entryIndex.toLinkLabel() + "${Constants.TAG_CMD}: " + entry.formatTags(", ")
+
+ /**
+ * Builds link label based on its index. e.g: L1
+ */
+ @JvmStatic
+ fun Int.toLinkLabel(): String = Constants.LINK_CMD + (this + 1)
+}
diff --git a/bin/main/net/thauvin/erik/mobibot/entries/EntryComment.kt b/bin/main/net/thauvin/erik/mobibot/entries/EntryComment.kt
new file mode 100644
index 0000000..e18d692
--- /dev/null
+++ b/bin/main/net/thauvin/erik/mobibot/entries/EntryComment.kt
@@ -0,0 +1,52 @@
+/*
+ * EntryComment.kt
+ *
+ * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net)
+ *
+ * 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 java.io.Serializable
+import java.time.LocalDateTime
+
+/**
+ * Entry comments data class.
+ */
+data class EntryComment(var comment: String, var nick: String) : Serializable {
+ /**
+ * Creation date.
+ */
+ val date: LocalDateTime = LocalDateTime.now()
+
+ override fun toString(): String = "EntryComment{comment='$comment', date=$date, nick='$nick'}"
+
+ companion object {
+ // Serial version UID
+ @Suppress("ConstPropertyName")
+ private const val serialVersionUID: Long = 1L
+ }
+}
diff --git a/bin/main/net/thauvin/erik/mobibot/entries/EntryLink.kt b/bin/main/net/thauvin/erik/mobibot/entries/EntryLink.kt
new file mode 100644
index 0000000..4a69446
--- /dev/null
+++ b/bin/main/net/thauvin/erik/mobibot/entries/EntryLink.kt
@@ -0,0 +1,213 @@
+/*
+ * EntryLink.kt
+ *
+ * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net)
+ *
+ * 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.SyndCategory
+import com.rometools.rome.feed.synd.SyndCategoryImpl
+import net.thauvin.erik.mobibot.commands.links.LinksManager
+import java.io.Serializable
+import java.util.*
+
+/**
+ * The class used to store link entries.
+ */
+class EntryLink(
+ // Link's comments
+ val comments: MutableList = mutableListOf(),
+
+ // Tags/categories
+ val tags: MutableList = mutableListOf(),
+
+ // Channel
+ var channel: String,
+
+ // Creation date
+ var date: Date = Calendar.getInstance().time,
+
+ // Link's URL
+ var link: String,
+
+ // Author's login
+ var login: String = "",
+
+ // Author's nickname
+ var nick: String,
+
+ // Link's title
+ var title: String
+) : Serializable {
+ /**
+ * Creates a new entry.
+ */
+ constructor(
+ link: String,
+ title: String,
+ nick: String,
+ login: String,
+ channel: String,
+ tags: List
+ ) : this(link = link, title = title, nick = nick, login = login, channel = channel) {
+ setTags(tags)
+ }
+
+ /**
+ * Creates a new entry.
+ */
+ constructor(
+ link: String,
+ title: String,
+ nick: String,
+ channel: String,
+ date: Date,
+ tags: List
+ ) : this(link = link, title = title, nick = nick, channel = channel, date = Date(date.time)) {
+ this.tags.addAll(tags)
+ }
+
+ /**
+ * Adds a new comment
+ */
+ fun addComment(comment: EntryComment): Int {
+ comments.add(comment)
+ return comments.lastIndex
+ }
+
+ /**
+ * Adds a new comment.
+ */
+ fun addComment(comment: String, nick: String): Int {
+ return addComment(EntryComment(comment, nick))
+ }
+
+ /**
+ * Deletes a specific comment.
+ */
+ 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)
+ }
+
+ /**
+ * Formats the tags.
+ */
+ fun formatTags(sep: String, prefix: String = ""): String {
+ return tags.joinToString(separator = sep, prefix = prefix) { it.name }
+ }
+
+ /**
+ * Returns a comment.
+ */
+ fun getComment(index: Int): EntryComment = comments[index]
+
+ /**
+ * Returns true if a string is contained in the link, title, or nick.
+ */
+ fun matches(match: String?): Boolean {
+ return if (match.isNullOrEmpty()) {
+ false
+ } else {
+ link.contains(match, true) || title.contains(match, true) || nick.contains(match, true)
+ }
+ }
+
+ /**
+ * Sets a comment.
+ */
+ fun setComment(index: Int, comment: String?, nick: String?) {
+ if (index < comments.size && !comment.isNullOrBlank() && !nick.isNullOrBlank()) {
+ comments[index] = EntryComment(comment, nick)
+ }
+ }
+
+ /**
+ * Sets the tags.
+ */
+ fun setTags(tags: String) {
+ setTags(tags.split(LinksManager.TAG_MATCH))
+ }
+
+ /**
+ * Sets the tags.
+ */
+ private fun setTags(tags: List) {
+ if (tags.isNotEmpty()) {
+ var category: SyndCategoryImpl
+ for (tag in tags) {
+ if (!tag.isNullOrBlank()) {
+ val t = tag.lowercase()
+ val mod = t[0]
+ if (mod == '-') {
+ // Don't remove the channel tag
+ if (channel.substring(1) != t.substring(1)) {
+ category = SyndCategoryImpl()
+ category.name = t.substring(1)
+ this.tags.remove(category)
+ }
+ } else {
+ category = SyndCategoryImpl()
+ if (mod == '+') {
+ category.name = t.substring(1)
+ } else {
+ category.name = t
+ }
+ if (!this.tags.contains(category)) {
+ this.tags.add(category)
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns a string representation of the object.
+ */
+ override fun toString(): String {
+ return ("EntryLink{channel='$channel', comments=$comments, date=$date, link='$link', login='$login'," +
+ "nick='$nick', tags=$tags, title='$title'}")
+ }
+
+ companion object {
+ // Serial version UID
+ @Suppress("ConstPropertyName")
+ private const val serialVersionUID: Long = 1L
+ }
+}
diff --git a/bin/main/net/thauvin/erik/mobibot/entries/FeedsManager.kt b/bin/main/net/thauvin/erik/mobibot/entries/FeedsManager.kt
new file mode 100644
index 0000000..f786cb2
--- /dev/null
+++ b/bin/main/net/thauvin/erik/mobibot/entries/FeedsManager.kt
@@ -0,0 +1,187 @@
+/*
+ * FeedsManager.kt
+ *
+ * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net)
+ *
+ * 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.*
+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.*
+import kotlin.io.path.exists
+
+/**
+ * Manages the RSS feeds.
+ */
+class FeedsManager private constructor() {
+ companion object {
+ private val logger: Logger = LoggerFactory.getLogger(FeedsManager::class.java)
+
+ // The file containing the current entries.
+ private const val CURRENT_XML = "current.xml"
+
+ // The .xml extension.
+ private const val DOT_XML = ".xml"
+
+ /**
+ * Loads the current feed.
+ */
+ @JvmStatic
+ @Throws(IOException::class, FeedException::class)
+ fun loadFeed(entries: Entries, currentFile: String = CURRENT_XML): 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
+ for (comment in description.value.split(" ")) {
+ 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.
+ */
+ @JvmStatic
+ fun saveFeed(entries: Entries, currentFile: String = CURRENT_XML) {
+ if (logger.isDebugEnabled) logger.debug("Saving the feeds...")
+ if (entries.logsDir.isNotBlank()) {
+ try {
+ val output = SyndFeedOutput()
+ val rss: SyndFeed = SyndFeedImpl()
+ val items: MutableList = 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 ")
+ .append(nick)
+ .append(" on ")
+ .append(channel)
+ .append("")
+ if (comments.isNotEmpty()) {
+ buff.append("