Moved to PircBotX and assertk.

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -35,13 +35,14 @@ package net.thauvin.erik.mobibot.commands
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import net.thauvin.erik.mobibot.FeedReader
import net.thauvin.erik.mobibot.Mobibot
import net.thauvin.erik.mobibot.Utils.helpFormat
import net.thauvin.erik.mobibot.Utils.sendMessage
import org.pircbotx.hooks.types.GenericMessageEvent
class ChannelFeed(bot: Mobibot, channel: String) : AbstractCommand(bot) {
class ChannelFeed(channel: String) : AbstractCommand() {
override val name = channel
override val help = listOf("To list the last 5 posts from the channel's weblog feed:", helpFormat("%c $channel"))
override val isOp = false
override val isOpOnly = false
override val isPublic = true
override val isVisible = true
@ -53,22 +54,16 @@ class ChannelFeed(bot: Mobibot, channel: String) : AbstractCommand(bot) {
initProperties(FEED_PROP)
}
override fun commandResponse(
sender: String,
login: String,
args: String,
isOp: Boolean,
isPrivate: Boolean
) {
override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
with(properties[FEED_PROP]) {
if (!isNullOrBlank()) {
runBlocking {
launch {
FeedReader(bot, sender, this@with).run()
FeedReader(this@with, event).run()
}
}
} else {
bot.send(sender, "There is no feed setup for this channel.", false)
event.sendMessage("There is no feed setup for this channel.")
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -32,27 +32,21 @@
package net.thauvin.erik.mobibot.commands
import net.thauvin.erik.mobibot.Mobibot
import net.thauvin.erik.mobibot.Utils.bot
import net.thauvin.erik.mobibot.Utils.helpFormat
import net.thauvin.erik.mobibot.Utils.isChannelOp
import org.pircbotx.hooks.types.GenericMessageEvent
class Me(bot: Mobibot) : AbstractCommand(bot) {
class Me : AbstractCommand() {
override val name = "me"
override val help = listOf("To have the bot perform an action:", helpFormat("%c $name <action>"))
override val isOp = true
override val isOpOnly = true
override val isPublic = false
override val isVisible = true
override fun commandResponse(
sender: String,
login: String,
args: String,
isOp: Boolean,
isPrivate: Boolean
) {
if (isOp) {
bot.action(args)
} else {
bot.helpDefault(sender, isOp, isPrivate)
override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
if (isChannelOp(channel, event)) {
event.bot().sendIRC().action(channel, args)
}
}
}

View file

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

View file

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

View file

@ -32,27 +32,21 @@
package net.thauvin.erik.mobibot.commands
import net.thauvin.erik.mobibot.Mobibot
import net.thauvin.erik.mobibot.Utils.bot
import net.thauvin.erik.mobibot.Utils.helpFormat
import net.thauvin.erik.mobibot.Utils.isChannelOp
import org.pircbotx.hooks.types.GenericMessageEvent
class Nick(bot: Mobibot) : AbstractCommand(bot) {
class Nick : AbstractCommand() {
override val name = "nick"
override val help = listOf("To change the bot's nickname:", helpFormat("%c $name <new_nick>"))
override val isOp = true
override val isOpOnly = true
override val isPublic = true
override val isVisible = true
override fun commandResponse(
sender: String,
login: String,
args: String,
isOp: Boolean,
isPrivate: Boolean
) {
if (isOp) {
bot.changeNick(args)
} else {
bot.helpDefault(sender, isOp, isPrivate)
override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
if (isChannelOp(channel, event)) {
event.bot().sendIRC().changeNick(args)
}
}
}

View file

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

View file

@ -32,28 +32,22 @@
package net.thauvin.erik.mobibot.commands
import net.thauvin.erik.mobibot.Mobibot
import net.thauvin.erik.mobibot.Utils.bot
import net.thauvin.erik.mobibot.Utils.helpFormat
import net.thauvin.erik.mobibot.Utils.isChannelOp
import org.pircbotx.hooks.types.GenericMessageEvent
class Say(bot: Mobibot) : AbstractCommand(bot) {
class Say : AbstractCommand() {
override val name = "say"
override val help = listOf("To have the bot say something on the channel:", helpFormat("%c $name <text>"))
override val isOp = true
override val isOpOnly = true
override val isPublic = false
override val isVisible = true
override fun commandResponse(
sender: String,
login: String,
args: String,
isOp: Boolean,
isPrivate: Boolean
) {
if (isOp) {
bot.send(args)
} else {
bot.helpDefault(sender, isOp, isPrivate)
override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
if (isChannelOp(channel, event)) {
event.bot().sendIRC().message(channel, args)
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -31,10 +31,10 @@
*/
package net.thauvin.erik.mobibot.modules
import net.thauvin.erik.mobibot.Mobibot
import net.thauvin.erik.mobibot.Utils.capitalise
import net.thauvin.erik.mobibot.Utils.encodeUrl
import net.thauvin.erik.mobibot.Utils.helpFormat
import net.thauvin.erik.mobibot.Utils.sendMessage
import net.thauvin.erik.mobibot.Utils.unescapeXml
import net.thauvin.erik.mobibot.Utils.urlReader
import net.thauvin.erik.mobibot.msg.ErrorMessage
@ -43,31 +43,34 @@ import net.thauvin.erik.mobibot.msg.NoticeMessage
import net.thauvin.erik.mobibot.msg.PublicMessage
import org.json.JSONException
import org.json.JSONObject
import org.pircbotx.hooks.types.GenericMessageEvent
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.io.IOException
import java.net.URL
/**
* The StockQuote module.
*/
class StockQuote(bot: Mobibot) : ThreadedModule(bot) {
class StockQuote : ThreadedModule() {
private val logger: Logger = LoggerFactory.getLogger(StockQuote::class.java)
/**
* Returns the specified stock quote from Alpha Vantage.
*/
override fun run(sender: String, cmd: String, args: String, isPrivate: Boolean) {
with(bot) {
if (args.isNotBlank()) {
try {
val messages = getQuote(args, properties[ALPHAVANTAGE_API_KEY_PROP])
for (msg in messages) {
send(sender, msg)
}
} catch (e: ModuleException) {
if (logger.isWarnEnabled) logger.warn(e.debugMessage, e)
send(e.message)
override fun run(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
if (args.isNotBlank()) {
try {
val messages = getQuote(args, properties[ALPHAVANTAGE_API_KEY_PROP])
for (msg in messages) {
event.sendMessage(channel, msg)
}
} else {
helpResponse(sender, isPrivate)
} catch (e: ModuleException) {
if (logger.isWarnEnabled) logger.warn(e.debugMessage, e)
event.sendMessage(e.message!!)
}
} else {
helpResponse(event)
}
}
@ -129,9 +132,9 @@ class StockQuote(bot: Mobibot) : ThreadedModule(bot) {
"${STOCK_CMD.capitalise()} is disabled. The API key is missing."
)
}
return if (symbol.isNotBlank()) {
val messages = mutableListOf<Message>()
if (symbol.isNotBlank()) {
val debugMessage = "getQuote($symbol)"
val messages = mutableListOf<Message>()
var response: String
try {
with(messages) {
@ -161,56 +164,55 @@ class StockQuote(bot: Mobibot) : ThreadedModule(bot) {
val quote = json.getJSONObject("Global Quote")
if (quote.isEmpty) {
add(ErrorMessage(INVALID_SYMBOL))
return messages
}
} else {
add(
PublicMessage(
"Symbol: " + unescapeXml(quote.getString("01. symbol"))
+ " [" + unescapeXml(symbolInfo.getString("2. name")) + ']'
add(
PublicMessage(
"Symbol: " + unescapeXml(quote.getString("01. symbol"))
+ " [" + unescapeXml(symbolInfo.getString("2. name")) + ']'
)
)
)
@Suppress("MagicNumber")
val pad = 10
val pad = 10
add(
PublicMessage(
"Price:".padEnd(pad).prependIndent()
+ unescapeXml(quote.getString("05. price"))
add(
PublicMessage(
"Price:".padEnd(pad).prependIndent()
+ unescapeXml(quote.getString("05. price"))
)
)
)
add(
PublicMessage(
"Previous:".padEnd(pad).prependIndent()
+ unescapeXml(quote.getString("08. previous close"))
add(
PublicMessage(
"Previous:".padEnd(pad).prependIndent()
+ unescapeXml(quote.getString("08. previous close"))
)
)
)
val data = arrayOf(
"Open" to "02. open",
"High" to "03. high",
"Low" to "04. low",
"Volume" to "06. volume",
"Latest" to "07. latest trading day"
)
val data = arrayOf(
"Open" to "02. open",
"High" to "03. high",
"Low" to "04. low",
"Volume" to "06. volume",
"Latest" to "07. latest trading day"
)
data.forEach {
add(
NoticeMessage(
"${it.first}:".padEnd(pad).prependIndent()
+ unescapeXml(quote.getString(it.second))
)
)
}
data.forEach {
add(
NoticeMessage(
"${it.first}:".padEnd(pad).prependIndent()
+ unescapeXml(quote.getString(it.second))
"Change:".padEnd(pad).prependIndent()
+ unescapeXml(quote.getString("09. change"))
+ " [" + unescapeXml(quote.getString("10. change percent")) + ']'
)
)
}
add(
NoticeMessage(
"Change:".padEnd(pad).prependIndent()
+ unescapeXml(quote.getString("09. change"))
+ " [" + unescapeXml(quote.getString("10. change percent")) + ']'
)
)
}
}
} catch (e: IOException) {
@ -218,10 +220,10 @@ class StockQuote(bot: Mobibot) : ThreadedModule(bot) {
} catch (e: NullPointerException) {
throw ModuleException(debugMessage, "An error has occurred retrieving a stock quote.", e)
}
messages
} else {
throw ModuleException(INVALID_SYMBOL)
messages.add(ErrorMessage(INVALID_SYMBOL))
}
return messages
}
}

View file

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

View file

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

View file

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

View file

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