Implemented seen command

This commit is contained in:
Erik C. Thauvin 2022-09-15 13:38:17 -07:00
parent d5000fa3c1
commit 3d49ebbd5f
11 changed files with 324 additions and 41 deletions

View file

@ -16,6 +16,11 @@
<option name="name" value="MavenLocal" />
<option name="url" value="file:$MAVEN_REPOSITORY$/" />
</remote-repository>
<remote-repository>
<option name="id" value="MavenLocal" />
<option name="name" value="MavenLocal" />
<option name="url" value="file:$MAVEN_REPOSITORY$/" />
</remote-repository>
<remote-repository>
<option name="id" value="maven2" />
<option name="name" value="maven2" />

3
.idea/misc.xml generated
View file

@ -1,5 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="FrameworkDetectionExcludesConfiguration">
<file type="web" url="file://$PROJECT_DIR$" />
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" project-jdk-name="18" project-jdk-type="JavaSDK" />
</project>

View file

@ -52,11 +52,13 @@
<ID>NestedBlockDepth:LinksMgr.kt$LinksMgr$override fun commandResponse(channel: String, args: String, event: GenericMessageEvent)</ID>
<ID>NestedBlockDepth:Lookup.kt$Lookup$override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent)</ID>
<ID>NestedBlockDepth:Posting.kt$Posting$override fun commandResponse(channel: String, args: String, event: GenericMessageEvent)</ID>
<ID>NestedBlockDepth:Seen.kt$Seen$fun add(nick: String)</ID>
<ID>NestedBlockDepth:Seen.kt$Seen$override fun commandResponse(channel: String, args: String, event: GenericMessageEvent)</ID>
<ID>NestedBlockDepth:StockQuote.kt$StockQuote.Companion$@JvmStatic @Throws(ModuleException::class) fun getQuote(symbol: String, apiKey: String?): List&lt;Message&gt;</ID>
<ID>NestedBlockDepth:Tell.kt$Tell$fun send(event: GenericUserEvent)</ID>
<ID>NestedBlockDepth:TellMessagesMgr.kt$TellMessagesMgr$@JvmStatic fun load(file: String): List&lt;TellMessage&gt;</ID>
<ID>NestedBlockDepth:TellMessagesMgr.kt$TellMessagesMgr$@JvmStatic fun save(file: String, messages: List&lt;TellMessage?&gt;?)</ID>
<ID>NestedBlockDepth:TwitterOAuth.kt$TwitterOAuth$@JvmStatic @Throws(TwitterException::class, IOException::class) fun main(args: Array&lt;String&gt;)</ID>
<ID>NestedBlockDepth:Utils.kt$Utils$@JvmStatic fun loadData(file: String, default: Any, logger: Logger, description: String): Any</ID>
<ID>NestedBlockDepth:Utils.kt$Utils$@JvmStatic fun saveData(file: String, data: Any, logger: Logger, description: String)</ID>
<ID>NestedBlockDepth:Weather2.kt$Weather2$override fun run(channel: String, cmd: String, args: String, event: GenericMessageEvent)</ID>
<ID>NestedBlockDepth:Weather2.kt$Weather2.Companion$@JvmStatic @Throws(ModuleException::class) fun getWeather(query: String, apiKey: String?): List&lt;Message&gt;</ID>
<ID>ReturnCount:Addons.kt$Addons$fun exec(channel: String, cmd: String, args: String, event: GenericMessageEvent): Boolean</ID>

View file

@ -65,6 +65,7 @@ import net.thauvin.erik.mobibot.commands.links.LinksMgr
import net.thauvin.erik.mobibot.commands.links.Posting
import net.thauvin.erik.mobibot.commands.links.Tags
import net.thauvin.erik.mobibot.commands.links.View
import net.thauvin.erik.mobibot.commands.seen.Seen
import net.thauvin.erik.mobibot.commands.tell.Tell
import net.thauvin.erik.mobibot.modules.Calc
import net.thauvin.erik.mobibot.modules.CryptoPrices
@ -113,7 +114,10 @@ class Mobibot(nickname: String, val channel: String, logsDirPath: String, p: Pro
// Commands and Modules
private val addons: Addons
// Tell module
// Seen command
private val seen: Seen
// Tell command
private val tell: Tell
/** Logger. */
@ -231,6 +235,8 @@ class Mobibot(nickname: String, val channel: String, logsDirPath: String, p: Pro
with(event.getBot<PircBotX>()) {
if (user.nick == nick) {
LinksMgr.twitter.notification("$nick has left ${event.channel.name} on irc://$serverHostname")
} else {
seen.add(user.nick)
}
}
}
@ -253,7 +259,7 @@ class Mobibot(nickname: String, val channel: String, logsDirPath: String, p: Pro
Constants.PROPS_ARG,
Constants.PROPS_ARG.substring(0, 1),
"Use alternate properties file"
).default("./mobibot.properties")
).default("./${ReleaseInfo.PROJECT}.properties")
val version by parser.option(
ArgType.Boolean,
Constants.VERSION_ARG,
@ -355,6 +361,7 @@ class Mobibot(nickname: String, val channel: String, logsDirPath: String, p: Pro
nickservCustomMessage = identMsg
}
isAutoReconnect = true
//socketConnectTimeout = Constants.CONNECT_TIMEOUT
//socketTimeout = Constants.CONNECT_TIMEOUT
//messageDelay = StaticDelay(500)
@ -388,6 +395,11 @@ class Mobibot(nickname: String, val channel: String, logsDirPath: String, p: Pro
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

View file

@ -38,18 +38,27 @@ 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.BufferedInputStream
import java.io.BufferedOutputStream
import java.io.BufferedReader
import java.io.IOException
import java.io.InputStreamReader
import java.io.ObjectInputStream
import java.io.ObjectOutputStream
import java.net.URL
import java.net.URLEncoder
import java.nio.charset.StandardCharsets
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.Date
import java.util.Properties
import java.util.stream.Collectors
import kotlin.io.path.exists
import kotlin.io.path.fileSize
/**
* Miscellaneous utilities.
@ -188,6 +197,29 @@ object Utils {
""
}
/**
* Load data.
*/
@JvmStatic
fun loadData(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 {@code true} if the list does not contain the given string.
*/
@ -238,6 +270,23 @@ object Utils {
@JvmStatic
fun String?.reverseColor(): String = colorize(Colors.REVERSE)
/**
* Save data
*/
@JvmStatic
fun saveData(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.
*/

View file

@ -63,7 +63,7 @@ class Info(private val tell: Tell) : AbstractCommand() {
*/
@JvmStatic
fun Long.toUptime(): String {
this.toDuration(DurationUnit.MILLISECONDS).toComponents { wholeDays, hours, minutes, _, _ ->
this.toDuration(DurationUnit.MILLISECONDS).toComponents { wholeDays, hours, minutes, seconds, _ ->
val years = wholeDays / 365
var days = wholeDays % 365
val months = days / 30
@ -88,7 +88,11 @@ class Info(private val tell: Tell) : AbstractCommand() {
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()
}

View file

@ -0,0 +1,118 @@
/*
* Seen.kt
*
* Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* Neither the name of this project nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.thauvin.erik.mobibot.commands.seen
import net.thauvin.erik.mobibot.Utils.bot
import net.thauvin.erik.mobibot.Utils.helpFormat
import net.thauvin.erik.mobibot.Utils.loadData
import net.thauvin.erik.mobibot.Utils.saveData
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.hooks.types.GenericMessageEvent
import org.slf4j.Logger
import org.slf4j.LoggerFactory
class Seen(private val serialObject: String) : AbstractCommand() {
private val logger: Logger = LoggerFactory.getLogger(Seen::class.java)
val seenNicks: MutableList<SeenNick> = mutableListOf()
override val name = "seen"
override val help = listOf("To view when a nickname was last seen:", helpFormat("%c $name <nick>"))
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)
ch.users.forEach {
if (args.equals(it.nick, true)) {
event.sendMessage("${it.nick} is on ${channel}.")
return
}
}
seenNicks.forEach {
if (it.nick.equals(args, true)) {
val lastSeen = System.currentTimeMillis() - it.last
event.sendMessage("${it.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.forEach {
if (it.nick.equals(nick, true)) {
if (it.nick != nick) it.nick = nick
it.last = System.currentTimeMillis()
save()
return
}
}
seenNicks.add(SeenNick(nick))
save()
}
}
fun clear() {
seenNicks.clear()
}
fun load() {
if (isEnabled()) {
@Suppress("UNCHECKED_CAST")
seenNicks += loadData(
serialObject,
mutableListOf<SeenNick>(),
logger,
"seen nicknames"
) as MutableList<SeenNick>
}
}
fun save() {
saveData(serialObject, seenNicks, logger, "seen nicknames")
}
init {
load()
}
}

View file

@ -0,0 +1,41 @@
/*
* SeenNick.kt
*
* Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* Neither the name of this project nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.thauvin.erik.mobibot.commands.seen
import java.io.Serializable
data class SeenNick(var nick: String, var last: Long = System.currentTimeMillis()) : Serializable {
companion object {
private const val serialVersionUID = 1L
}
}

View file

@ -31,18 +31,12 @@
*/
package net.thauvin.erik.mobibot.commands.tell
import net.thauvin.erik.mobibot.Utils.loadData
import net.thauvin.erik.mobibot.Utils.saveData
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.io.BufferedInputStream
import java.io.BufferedOutputStream
import java.io.IOException
import java.io.ObjectInputStream
import java.io.ObjectOutputStream
import java.nio.file.Files
import java.nio.file.Paths
import java.time.Clock
import java.time.LocalDateTime
import kotlin.io.path.exists
/**
* The Tell Messages Manager.
@ -65,23 +59,8 @@ object TellMessagesMgr {
*/
@JvmStatic
fun load(file: String): List<TellMessage> {
val serialFile = Paths.get(file)
if (serialFile.exists()) {
try {
ObjectInputStream(
BufferedInputStream(Files.newInputStream(serialFile))
).use { input ->
if (logger.isDebugEnabled) logger.debug("Loading the messages.")
@Suppress("UNCHECKED_CAST")
return input.readObject() as List<TellMessage>
}
} catch (e: IOException) {
logger.error("An IO error occurred loading the messages queue.", e)
} catch (e: ClassNotFoundException) {
logger.error("An error occurred loading the messages queue.", e)
}
}
return listOf()
return loadData(file, emptyList<TellMessage>(), logger, "message queue") as List<TellMessage>
}
/**
@ -89,15 +68,8 @@ object TellMessagesMgr {
*/
@JvmStatic
fun save(file: String, messages: List<TellMessage?>?) {
try {
BufferedOutputStream(Files.newOutputStream(Paths.get(file))).use { bos ->
ObjectOutputStream(bos).use { output ->
if (logger.isDebugEnabled) logger.debug("Saving the messages.")
output.writeObject(messages)
}
}
} catch (e: IOException) {
logger.error("Unable to save messages queue.", e)
if (messages != null) {
saveData(file, messages, logger, "messages")
}
}
}

View file

@ -46,6 +46,8 @@ class InfoTest {
assertThat(1320300000L.toUptime(), "weeks days hours minutes").isEqualTo("2 weeks 1 day 6 hours 45 minutes")
assertThat(2700000L.toUptime(), "45 minutes").isEqualTo("45 minutes")
assertThat(60000L.toUptime(), "1 minute").isEqualTo("1 minute")
assertThat(0L.toUptime(), "0 minute").isEqualTo("0 minute")
assertThat(59000L.toUptime(), "59 seconds").isEqualTo("59 seconds")
assertThat(0L.toUptime(), "0 second").isEqualTo("0 second")
}
}

View file

@ -0,0 +1,75 @@
/*
* SeenTest.kt
*
* Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* Neither the name of this project nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.thauvin.erik.mobibot.commands.seen
import assertk.assertThat
import assertk.assertions.isEqualTo
import assertk.assertions.isGreaterThan
import assertk.assertions.isTrue
import org.testng.annotations.AfterClass
import org.testng.annotations.BeforeClass
import org.testng.annotations.Test
import kotlin.io.path.deleteIfExists
import kotlin.io.path.fileSize
class SeenTest {
private val tmpFile = kotlin.io.path.createTempFile(suffix = ".ser")
private val seen = Seen(tmpFile.toAbsolutePath().toString())
@BeforeClass
fun saveTest() {
seen.add("ErikT")
assertThat(tmpFile.fileSize(), "temporary file is empty").isGreaterThan(0)
}
@AfterClass(alwaysRun = true)
fun afterClass() {
tmpFile.deleteIfExists()
}
@Test
fun loadTest() {
val nick = seen.seenNicks[0]
seen.clear()
seen.load()
assertThat(seen.seenNicks[0] == nick, "nick is different").isTrue()
}
@Test(priority = 10)
fun clearTest() {
seen.clear()
seen.save()
seen.load()
assertThat(seen.seenNicks.size, "nicknames are not empty.").isEqualTo(0)
}
}