- * and follow the prompts/instructions.
- *
- * @author Erik C. Thauvin
- * @author Twitter4J
- * @created Sep 13, 2010
- * @since 1.0
- */
-@SuppressWarnings("PMD.UseUtilityClass")
-public final class TwitterOAuth {
- /**
- * Twitter OAuth Client Registration.
- *
- * @param args The consumerKey and consumerSecret should be passed as arguments.
- * @throws TwitterException If an error occurs.
- * @throws IOException If an IO error occurs.
- */
- @SuppressFBWarnings({"DM_DEFAULT_ENCODING", "IMC_IMMATURE_CLASS_PRINTSTACKTRACE"})
- @SuppressWarnings({"PMD.AvoidPrintStackTrace", "PMD.SystemPrintln"})
- public static void main(final String[] args) throws TwitterException, IOException {
- if (args.length == 2) {
- final twitter4j.Twitter twitter = new TwitterFactory().getInstance();
- twitter.setOAuthConsumer(args[0], args[1]);
- final RequestToken requestToken = twitter.getOAuthRequestToken();
- AccessToken accessToken = null;
- try (final BufferedReader br = new BufferedReader(new InputStreamReader(System.in))) {
- while (null == accessToken) {
- System.out.println("Open the following URL and grant access to your account:");
- System.out.println(requestToken.getAuthorizationURL());
- System.out.print("Enter the PIN (if available) or just hit enter.[PIN]:");
- final String pin = br.readLine();
- try {
- if (pin != null && pin.length() > 0) {
- accessToken = twitter.getOAuthAccessToken(requestToken, pin);
- } else {
- accessToken = twitter.getOAuthAccessToken();
- }
-
- System.out.println(
- "Please add the following to the bot's property file:" + "\n\n" + "twitter-consumerKey="
- + args[0] + '\n' + "twitter-consumerSecret=" + args[1] + '\n' + "twitter-token="
- + accessToken.getToken() + '\n' + "twitter-tokenSecret=" + accessToken.getTokenSecret());
- } catch (TwitterException te) {
- if (401 == te.getStatusCode()) {
- System.out.println("Unable to get the access token.");
- } else {
- te.printStackTrace();
- }
- }
- }
- }
- } else {
- System.out.println("Usage: " + TwitterOAuth.class.getName() + " ");
- }
-
- System.exit(0);
- }
-}
diff --git a/src/main/java/net/thauvin/erik/mobibot/Utils.java b/src/main/java/net/thauvin/erik/mobibot/Utils.java
deleted file mode 100644
index 1489ecf..0000000
--- a/src/main/java/net/thauvin/erik/mobibot/Utils.java
+++ /dev/null
@@ -1,317 +0,0 @@
-/*
- * Utils.java
- *
- * Copyright (c) 2004-2020, 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;
-
-import org.apache.commons.lang3.StringUtils;
-import org.jibble.pircbot.Colors;
-import org.jsoup.Jsoup;
-
-import java.io.File;
-import java.time.LocalDateTime;
-import java.time.ZoneId;
-import java.time.format.DateTimeFormatter;
-import java.util.Date;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Miscellaneous utilities class.
- *
- * @author Erik C. Thauvin
- * @created 2014-04-26
- * @since 1.0
- */
-public final class Utils {
- /**
- * Disables the default constructor.
- *
- * @throws UnsupportedOperationException If the constructor is called.
- */
- private Utils() {
- throw new UnsupportedOperationException("Illegal constructor call.");
- }
-
- /**
- * Makes the given int bold.
- *
- * @param i The int.
- * @return The bold string.
- */
- public static String bold(final int i) {
- return bold(Integer.toString(i));
- }
-
- /**
- * Makes the given string bold.
- *
- * @param s The string.
- * @return The bold string.
- */
- public static String bold(final String s) {
- return colorize(s, Colors.BOLD);
- }
-
- /**
- * Colorize a string.
- *
- * @param s The string.
- * @param color The color.
- * @return The colorized string.
- */
- static String colorize(final String s, final String color) {
- if (s == null) {
- return Colors.NORMAL;
- } else if (Colors.BOLD.equals(color) || Colors.REVERSE.equals(color)) {
- return color + s + color;
- }
-
- return color + s + Colors.NORMAL;
- }
-
- /**
- * Makes the given string cyan.
- *
- * @param s The string.
- * @return The cyan string.
- */
- public static String cyan(final String s) {
- return colorize(s, Colors.CYAN);
- }
-
- /**
- * Ensures that the given location (File/URL) has a trailing slash (/) to indicate a directory.
- *
- * @param location The File or URL location.
- * @param isUrl Set to true if the location is a URL
- * @return The location ending with a slash.
- */
- static String ensureDir(final String location, final boolean isUrl) {
- if (isUrl) {
- if (location.charAt(location.length() - 1) == '/') {
- return location;
- } else {
- return location + '/';
- }
- } else {
- if (location.charAt(location.length() - 1) == File.separatorChar) {
- return location;
- } else {
- return location + File.separatorChar;
- }
- }
- }
-
- /**
- * Returns a property as an int.
- *
- * @param property The property value.
- * @param def The default property value.
- * @return The port or default value if invalid.
- */
- public static int getIntProperty(final String property, final int def) {
- int prop;
-
- try {
- prop = Integer.parseInt(property);
- } catch (NumberFormatException ignore) {
- prop = def;
- }
-
- return prop;
- }
-
- /**
- * Makes the given string green.
- *
- * @param s The string.
- * @return The green string.
- */
- public static String green(final String s) {
- return colorize(s, Colors.DARK_GREEN);
- }
-
- /**
- * Returns the specified date as an ISO local date string.
- *
- * @param date The date.
- * @return The date in {@link DateTimeFormatter#ISO_LOCAL_DATE} format.
- */
- public static String isoLocalDate(final Date date) {
- return isoLocalDate(LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault()));
- }
-
- /**
- * Returns the specified date as an ISO local date string.
- *
- * @param date The date.
- * @return The date in {@link DateTimeFormatter#ISO_LOCAL_DATE} format.
- */
- public static String isoLocalDate(final LocalDateTime date) {
- return date.format(DateTimeFormatter.ISO_LOCAL_DATE);
- }
-
- /**
- * Obfuscates the given string.
- *
- * @param s The string.
- * @return The obfuscated string.
- */
- public static String obfuscate(final String s) {
- if (StringUtils.isNotBlank(s)) {
- return StringUtils.repeat('x', s.length());
- }
- return s;
- }
-
- /**
- * Returns the plural form of a word, if count > 1.
- *
- * @param count The count.
- * @param word The word.
- * @param plural The plural word.
- * @return The plural string.
- */
- public static String plural(final long count, final String word, final String plural) {
- if (count > 1) {
- return plural;
- } else {
- return word;
- }
- }
-
- /**
- * Makes the given string red.
- *
- * @param s The string.
- * @return The red string.
- */
- public static String red(final String s) {
- return colorize(s, Colors.RED);
- }
-
- /**
- * Makes the given string reverse color.
- *
- * @param s The string.
- * @return The reverse color string.
- */
- public static String reverseColor(final String s) {
- return colorize(s, Colors.REVERSE);
- }
-
- /**
- * Returns today's date.
- *
- * @return Today's date in {@link DateTimeFormatter#ISO_LOCAL_DATE} format.
- */
- public static String today() {
- return isoLocalDate(LocalDateTime.now());
- }
-
- /**
- * Converts XML/XHTML entities to plain text.
- *
- * @param str The string to unescape.
- * @return The unescaped string.
- */
- public static String unescapeXml(final String str) {
- return Jsoup.parse(str).text();
- }
-
- /**
- * Converts milliseconds to year month week day hour and minutes.
- *
- * @param uptime The uptime in milliseconds.
- * @return The uptime in year month week day hours and minutes.
- */
- public static String uptime(final long uptime) {
- final StringBuilder info = new StringBuilder();
-
- long days = TimeUnit.MILLISECONDS.toDays(uptime);
- final long years = days / 365;
- days %= 365;
- final long months = days / 30;
- days %= 30;
- final long weeks = days / 7;
- days %= 7;
- final long hours = TimeUnit.MILLISECONDS.toHours(uptime) - TimeUnit.DAYS.toHours(
- TimeUnit.MILLISECONDS.toDays(uptime));
- final long minutes = TimeUnit.MILLISECONDS.toMinutes(uptime) - TimeUnit.HOURS.toMinutes(
- TimeUnit.MILLISECONDS.toHours(uptime));
-
- if (years > 0) {
- info.append(years).append(plural(years, " year ", " years "));
- }
-
- if (months > 0) {
- info.append(weeks).append(plural(months, " month ", " months "));
- }
-
- if (weeks > 0) {
- info.append(weeks).append(plural(weeks, " week ", " weeks "));
- }
-
-
- if (days > 0) {
- info.append(days).append(plural(days, " day ", " days "));
- }
-
- if (hours > 0) {
- info.append(hours).append(plural(hours, " hour ", " hours "));
- }
-
- info.append(minutes).append(plural(minutes, " minute", " minutes"));
-
- return info.toString();
- }
-
- /**
- * Returns the specified date formatted as yyyy-MM-dd HH:mm.
- *
- * @param date The date.
- * @return The formatted date.
- */
- public static String utcDateTime(final Date date) {
- return utcDateTime(LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault()));
- }
-
- /**
- * Returns the specified date formatted as yyyy-MM-dd HH:mm.
- *
- * @param date The date.
- * @return The formatted date.
- */
- public static String utcDateTime(final LocalDateTime date) {
- return date.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"));
- }
-
-}
diff --git a/src/main/java/net/thauvin/erik/mobibot/entries/EntriesMgr.java b/src/main/java/net/thauvin/erik/mobibot/entries/EntriesMgr.java
deleted file mode 100644
index d1ba70b..0000000
--- a/src/main/java/net/thauvin/erik/mobibot/entries/EntriesMgr.java
+++ /dev/null
@@ -1,338 +0,0 @@
-/*
- * EntriesMgr.java
- *
- * Copyright (c) 2004-2020, 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.SyndContent;
-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 edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
-import net.thauvin.erik.mobibot.Mobibot;
-import net.thauvin.erik.mobibot.Utils;
-import org.apache.commons.lang3.StringUtils;
-
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.io.OutputStreamWriter;
-import java.io.Writer;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-import java.nio.file.Paths;
-import java.util.ArrayList;
-import java.util.Calendar;
-import java.util.Collection;
-import java.util.List;
-
-/**
- * Manages the feed entries.
- *
- * @author Erik C. Thauvin
- * @created 2014-04-28
- * @since 1.0
- */
-public final class EntriesMgr {
- /**
- * The name of the file containing the current entries.
- */
- public static final String CURRENT_XML = "current.xml";
-
- /**
- * The name of the file containing the backlog entries.
- */
- public static final String NAV_XML = "nav.xml";
-
- /**
- * The .xml extension
- */
- public static final String XML_EXT = ".xml";
-
- // Maximum number of backlogs to keep
- private static final int MAX_BACKLOGS = 10;
-
- /**
- * Disables the default constructor.
- *
- * @throws UnsupportedOperationException If the constructor is called.
- */
- private EntriesMgr() {
- throw new UnsupportedOperationException("Illegal constructor call.");
- }
-
- /**
- * Loads the backlogs.
- *
- * @param file The file containing the backlogs.
- * @param history The history list.
- * @throws IOException If the file was not found or could not be read.
- * @throws FeedException If an error occurred while reading the feed.
- */
- public static void loadBacklogs(final String file, final Collection history)
- throws IOException, FeedException {
- history.clear();
-
- final SyndFeedInput input = new SyndFeedInput();
-
- try (final InputStreamReader reader =
- new InputStreamReader(Files.newInputStream(Paths.get(file)), StandardCharsets.UTF_8)) {
-
- final SyndFeed feed = input.build(reader);
-
- final List items = feed.getEntries();
- SyndEntry item;
-
- for (int i = items.size() - 1; i >= 0; i--) {
- item = items.get(i);
- history.add(item.getTitle());
- }
- }
- }
-
- /**
- * Loads the current entries.
- *
- * @param file The file containing the current entries.
- * @param channel The channel
- * @param entries The entries.
- * @return The feed's last published date.
- * @throws IOException If the file was not found or could not be read.
- * @throws FeedException If an error occurred while reading the feed.
- */
- @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
- public static String loadEntries(final String file, final String channel, final Collection entries)
- throws IOException, FeedException {
- entries.clear();
-
- final SyndFeedInput input = new SyndFeedInput();
-
- final String today;
-
- try (final InputStreamReader reader = new InputStreamReader(
- Files.newInputStream(Paths.get(file)), StandardCharsets.UTF_8)) {
- final SyndFeed feed = input.build(reader);
-
- today = Utils.isoLocalDate(feed.getPublishedDate());
-
- final List items = feed.getEntries();
- SyndEntry item;
- SyndContent description;
- String[] comments;
- String author;
- EntryLink entry;
-
- for (int i = items.size() - 1; i >= 0; i--) {
- item = items.get(i);
- author = item.getAuthor()
- .substring(item.getAuthor().lastIndexOf('(') + 1, item.getAuthor().length() - 1);
- entry = new EntryLink(item.getLink(),
- item.getTitle(),
- author,
- channel,
- item.getPublishedDate(),
- item.getCategories());
- description = item.getDescription();
- comments = description.getValue().split(" ");
-
- int split;
- for (final String comment : comments) {
- split = comment.indexOf(": ");
-
- if (split != -1) {
- entry.addComment(comment.substring(split + 2).trim(), comment.substring(0, split).trim());
- }
- }
-
- entries.add(entry);
- }
- }
-
- return today;
- }
-
- /**
- * Saves the entries.
- *
- * @param bot The bot object.
- * @param entries The entries array.
- * @param history The history array.
- * @param isDayBackup Set the true if the daily backup file should also be created.
- */
- @SuppressFBWarnings(value = {"CE_CLASS_ENVY", "CC_CYCLOMATIC_COMPLEXITY"}, justification = "Yes, it does.")
- @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
- public static void saveEntries(final Mobibot bot,
- final List entries,
- final List history,
- final boolean isDayBackup) {
- if (bot.getLogger().isDebugEnabled()) {
- bot.getLogger().debug("Saving the feeds...");
- }
-
- if (StringUtils.isNotBlank(bot.getLogsDir()) && StringUtils.isNotBlank(bot.getWeblogUrl())) {
- try {
- final SyndFeedOutput output = new SyndFeedOutput();
- SyndFeed rss = new SyndFeedImpl();
- final List items = new ArrayList<>(0);
- SyndEntry item;
- SyndContent description;
- try (final Writer fw = new OutputStreamWriter(
- Files.newOutputStream(Paths.get(bot.getLogsDir() + CURRENT_XML)), StandardCharsets.UTF_8)) {
- rss.setFeedType("rss_2.0");
- rss.setTitle(bot.getChannel() + " IRC Links");
- rss.setDescription("Links from " + bot.getIrcServer() + " on " + bot.getChannel());
- rss.setLink(bot.getWeblogUrl());
- rss.setPublishedDate(Calendar.getInstance().getTime());
- rss.setLanguage("en");
-
- EntryLink entry;
- StringBuilder buff;
- EntryComment comment;
-
- for (int i = (entries.size() - 1); i >= 0; --i) {
- entry = entries.get(i);
-
- buff = new StringBuilder()
- .append("Posted by ")
- .append(entry.getNick())
- .append(" on ")
- .append(entry.getChannel())
- .append("");
-
- if (entry.getCommentsCount() > 0) {
- buff.append("
");
-
- final EntryComment[] comments = entry.getComments();
-
- for (int j = 0; j < comments.length; j++) {
- comment = comments[j];
-
- if (j > 0) {
- buff.append(" ");
- }
-
- buff.append(comment.getNick()).append(": ").append(comment.getComment());
- }
- }
-
- item = new SyndEntryImpl();
- item.setLink(entry.getLink());
- description = new SyndContentImpl();
- description.setValue(buff.toString());
- item.setDescription(description);
- item.setTitle(entry.getTitle());
- item.setPublishedDate(entry.getDate());
- item.setAuthor(
- bot.getChannel().substring(1) + '@' + bot.getIrcServer() + " (" + entry.getNick() + ')');
- item.setCategories(entry.getTags());
-
- items.add(item);
- }
-
- rss.setEntries(items);
-
- if (bot.getLogger().isDebugEnabled()) {
- bot.getLogger().debug("Writing the entries feed.");
- }
-
- output.output(rss, fw);
- }
-
- try (final Writer fw = new OutputStreamWriter(
- Files.newOutputStream(Paths.get(
- bot.getLogsDir() + bot.getToday() + XML_EXT)), StandardCharsets.UTF_8)) {
- output.output(rss, fw);
- }
-
- if (isDayBackup) {
- if (StringUtils.isNotBlank(bot.getBacklogsUrl())) {
- if (!history.contains(bot.getToday())) {
- history.add(bot.getToday());
-
- while (history.size() > MAX_BACKLOGS) {
- history.remove(0);
- }
- }
-
- try (final Writer fw = new OutputStreamWriter(
- Files.newOutputStream(Paths.get(bot.getLogsDir() + NAV_XML)), StandardCharsets.UTF_8)) {
- rss = new SyndFeedImpl();
- rss.setFeedType("rss_2.0");
- rss.setTitle(bot.getChannel() + " IRC Links Backlogs");
- rss.setDescription("Backlogs of Links from " + bot.getIrcServer() + " on "
- + bot.getChannel());
- rss.setLink(bot.getBacklogsUrl());
- rss.setPublishedDate(Calendar.getInstance().getTime());
-
- String date;
- items.clear();
-
- for (int i = (history.size() - 1); i >= 0; --i) {
- date = history.get(i);
-
- item = new SyndEntryImpl();
- item.setLink(bot.getBacklogsUrl() + date + ".xml");
- item.setTitle(date);
- description = new SyndContentImpl();
- description.setValue("Links for " + date);
- item.setDescription(description);
-
- items.add(item);
- }
-
- rss.setEntries(items);
-
- if (bot.getLogger().isDebugEnabled()) {
- bot.getLogger().debug("Writing the backlog feed.");
- }
-
- output.output(rss, fw);
- }
- } else {
- bot.getLogger().warn("Unable to generate the backlogs feed. No property configured.");
- }
- }
- } catch (FeedException | IOException e) {
- bot.getLogger().warn("Unable to generate the entries feed.", e);
- }
- } else {
- bot.getLogger()
- .warn("Unable to generate the entries feed. At least one of the required property is missing.");
- }
- }
-}
diff --git a/src/main/java/net/thauvin/erik/mobibot/entries/EntriesUtils.java b/src/main/java/net/thauvin/erik/mobibot/entries/EntriesUtils.java
deleted file mode 100644
index 636fbe4..0000000
--- a/src/main/java/net/thauvin/erik/mobibot/entries/EntriesUtils.java
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
- * EntriesUtils.java
- *
- * Copyright (c) 2004-2019, 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 edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
-import net.thauvin.erik.mobibot.Commands;
-import net.thauvin.erik.mobibot.Constants;
-import net.thauvin.erik.mobibot.Utils;
-
-/**
- * The Utils class.
- *
- * @author Erik C. Thauvin
- * @created 2019-04-19
- * @since 1.0
- */
-public final class EntriesUtils {
- /**
- * Disables the default constructor.
- */
- private EntriesUtils() {
- throw new UnsupportedOperationException("Illegal constructor call.");
- }
-
- /**
- * Builds an entry's comment for display on the channel.
- *
- * @param entryIndex The entry's index.
- * @param commentIndex The comment's index.
- * @param comment The {@link EntryComment comment} object.
- * @return The entry's comment.
- */
- public static String buildComment(final int entryIndex, final int commentIndex, final EntryComment comment) {
- return (Commands.LINK_CMD + (entryIndex + 1) + '.' + (commentIndex + 1) + ": [" + comment.getNick() + "] "
- + comment.getComment());
- }
-
- /**
- * Builds an entry's link for display on the channel.
- *
- * @param index The entry's index.
- * @param entry The {@link EntryLink entry} object.
- * @return The entry's link.
- * @see #buildLink(int, EntryLink, boolean)
- */
- public static String buildLink(final int index, final EntryLink entry) {
- return buildLink(index, entry, false);
- }
-
- /**
- * Builds an entry's link for display on the channel.
- *
- * @param index The entry's index.
- * @param entry The {@link EntryLink entry} object.
- * @param isView Set to true to display the number of comments.
- * @return The entry's link.
- */
- @SuppressFBWarnings(value = "CE_CLASS_ENVY", justification = "Yes, it does.")
- public static String buildLink(final int index, final EntryLink entry, final boolean isView) {
- final StringBuilder buff = new StringBuilder().append(Commands.LINK_CMD).append(index + 1)
- .append(": ").append('[').append(entry.getNick()).append(']');
-
- if (isView && entry.hasComments()) {
- buff.append("[+").append(entry.getCommentsCount()).append(']');
- }
-
- buff.append(' ');
-
- if (Constants.NO_TITLE.equals(entry.getTitle())) {
- buff.append(entry.getTitle());
- } else {
- buff.append(Utils.bold(entry.getTitle()));
- }
-
- buff.append(" ( ").append(Utils.green(entry.getLink())).append(" )");
-
- return buff.toString();
- }
-
- /**
- * Build an entry's tags/categories for display on the channel.
- *
- * @param entryIndex The entry's index.
- * @param entry The {@link EntryLink entry} object.
- * @return The entry's tags.
- */
- public static String buildTags(final int entryIndex, final EntryLink entry) {
- return (Commands.LINK_CMD + (entryIndex + 1) + "T: " + entry.getPinboardTags().replace(",", ", "));
- }
-}
diff --git a/src/main/java/net/thauvin/erik/mobibot/entries/EntryComment.java b/src/main/java/net/thauvin/erik/mobibot/entries/EntryComment.java
deleted file mode 100644
index 1661350..0000000
--- a/src/main/java/net/thauvin/erik/mobibot/entries/EntryComment.java
+++ /dev/null
@@ -1,121 +0,0 @@
-/*
- * EntryComment.java
- *
- * Copyright (c) 2004-2020, 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 java.io.Serializable;
-import java.time.LocalDateTime;
-
-/**
- * The class used to store comments associated to a specific entry.
- *
- * @author Erik C. Thauvin
- * @created Jan 31, 2004
- * @since 1.0
- */
-@SuppressWarnings({"PMD.DataClass"})
-public class EntryComment implements Serializable {
- // Serial version UID
- static final long serialVersionUID = 1L;
-
- // Creation date
- private final LocalDateTime date = LocalDateTime.now();
-
- private String comment = "";
- private String nick = "";
-
- /**
- * Creates a new comment.
- *
- * @param comment The new comment.
- * @param nick The nickname of the comment's author.
- */
- public EntryComment(final String comment, final String nick) {
- this.comment = comment;
- this.nick = nick;
- }
-
- /**
- * Creates a new comment.
- */
- @SuppressWarnings("UnusedDeclaration")
- protected EntryComment() {
- // Required for serialization
- }
-
- /**
- * Returns the comment.
- *
- * @return The comment.
- */
- public final String getComment() {
- return comment;
- }
-
- /**
- * Sets the comment.
- *
- * @param comment The actual comment.
- */
- @SuppressWarnings("UnusedDeclaration")
- public final void setComment(final String comment) {
- this.comment = comment;
- }
-
- /**
- * Returns the comment's creation date.
- *
- * @return The date.
- */
- @SuppressWarnings("UnusedDeclaration")
- public final LocalDateTime getDate() {
- return date;
- }
-
- /**
- * Returns the nickname of the author of the comment.
- *
- * @return The nickname.
- */
- public final String getNick() {
- return nick;
- }
-
- /**
- * Sets the nickname of the author of the comment.
- *
- * @param nick The new nickname.
- */
- public final void setNick(final String nick) {
- this.nick = nick;
- }
-}
diff --git a/src/main/java/net/thauvin/erik/mobibot/entries/EntryLink.java b/src/main/java/net/thauvin/erik/mobibot/entries/EntryLink.java
deleted file mode 100644
index 0365ae4..0000000
--- a/src/main/java/net/thauvin/erik/mobibot/entries/EntryLink.java
+++ /dev/null
@@ -1,406 +0,0 @@
-/*
- * EntryLink.java
- *
- * Copyright (c) 2004-2020, 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.SyndCategory;
-import com.rometools.rome.feed.synd.SyndCategoryImpl;
-import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
-import net.thauvin.erik.mobibot.Constants;
-
-import java.io.Serializable;
-import java.util.Calendar;
-import java.util.Date;
-import java.util.List;
-import java.util.concurrent.CopyOnWriteArrayList;
-
-/**
- * The class used to store link entries.
- *
- * @author Erik C. Thauvin
- * @created Jan 31, 2004
- * @since 1.0
- */
-public class EntryLink implements Serializable {
- // Serial version UID
- static final long serialVersionUID = 1L;
-
- // Link's comments
- private final List comments = new CopyOnWriteArrayList<>();
-
- // Tags/categories
- private final List tags = new CopyOnWriteArrayList<>();
-
- // Channel
- private String channel;
-
- // Creation date
- private Date date = Calendar.getInstance().getTime();
-
- // Link's URL
- private String link;
-
- // Author's login
- private String login = "";
-
- // Author's nickname
- private String nick;
-
- // Link's title
- private String title;
-
- /**
- * Creates a new entry.
- *
- * @param link The new entry's link.
- * @param title The new entry's title.
- * @param nick The nickname of the author of the link.
- * @param login The login of the author of the link.
- * @param channel The channel.
- * @param tags The entry's tags/categories.
- */
- public EntryLink(final String link,
- final String title,
- final String nick,
- final String login,
- final String channel,
- final String tags) {
- this.link = link;
- this.title = title;
- this.nick = nick;
- this.login = login;
- this.channel = channel;
-
- setTags(tags);
- }
-
- /**
- * Creates a new entry.
- *
- * @param link The new entry's link.
- * @param title The new entry's title.
- * @param nick The nickname of the author of the link.
- * @param channel The channel.
- * @param date The entry date.
- * @param tags The entry's tags/categories.
- */
- public EntryLink(final String link,
- final String title,
- final String nick,
- final String channel,
- final Date date,
- final List tags) {
- this.link = link;
- this.title = title;
- this.nick = nick;
- this.channel = channel;
- this.date = new Date(date.getTime());
-
- setTags(tags);
- }
-
- /**
- * Adds a new comment.
- *
- * @param comment The actual comment.
- * @param nick The nickname of the author of the comment.
- * @return The total number of comments for this entry.
- */
- public final int addComment(final String comment, final String nick) {
- comments.add(new EntryComment(comment, nick));
-
- return (comments.size() - 1);
- }
-
- /**
- * Deletes a specific comment.
- *
- * @param index The index of the comment to delete.
- */
- public final void deleteComment(final int index) {
- if (index < comments.size()) {
- comments.remove(index);
- }
- }
-
- /**
- * Returns the channel the link was posted on.
- *
- * @return The channel
- */
- public final String getChannel() {
- return channel;
- }
-
- /**
- * Sets the channel.
- *
- * @param channel The channel.
- */
- @SuppressWarnings("UnusedDeclaration")
- public final void setChannel(final String channel) {
- this.channel = channel;
- }
-
- /**
- * Returns a comment.
- *
- * @param index The comment's index.
- * @return The specific comment.
- */
- public final EntryComment getComment(final int index) {
- return (comments.get(index));
- }
-
- /**
- * Returns all the comments.
- *
- * @return The comments.
- */
- public final EntryComment[] getComments() {
- return (comments.toArray(new EntryComment[0]));
- }
-
- /**
- * Returns the total number of comments.
- *
- * @return The count of comments.
- */
- public final int getCommentsCount() {
- return comments.size();
- }
-
- /**
- * Returns the comment's creation date.
- *
- * @return The date.
- */
- public final Date getDate() {
- return new Date(date.getTime());
- }
-
- /**
- * Returns the comment's link.
- *
- * @return The link.
- */
- public final String getLink() {
- return link;
- }
-
- /**
- * Sets the comment's link.
- *
- * @param link The new link.
- */
- public final void setLink(final String link) {
- this.link = link;
- }
-
- /**
- * Returns the comment's author login.
- *
- * @return The login;
- */
- public final String getLogin() {
- return login;
- }
-
- /**
- * Sets the comment's author login.
- *
- * @param login The new login.
- */
- @SuppressWarnings("UnusedDeclaration")
- public final void setLogin(final String login) {
- this.login = login;
- }
-
- /**
- * Returns the comment's author nickname.
- *
- * @return The nickname.
- */
- public final String getNick() {
- return nick;
- }
-
- /**
- * Sets the comment's author nickname.
- *
- * @param nick The new nickname.
- */
- public final void setNick(final String nick) {
- this.nick = nick;
- }
-
- /**
- * Returns the tags formatted for pinboard.in
- *
- * @return The tags as a comma-delimited string.
- */
- public final String getPinboardTags() {
- final StringBuilder pinboardTags = new StringBuilder(nick);
-
- for (final SyndCategory tag : tags) {
- pinboardTags.append(',');
- pinboardTags.append(tag.getName());
- }
-
- return pinboardTags.toString();
- }
-
- /**
- * Returns the tags.
- *
- * @return The tags.
- */
- public final List getTags() {
- return tags;
- }
-
- /**
- * Sets the tags.
- *
- * @param tags The space-delimited tags.
- */
- @SuppressFBWarnings("STT_STRING_PARSING_A_FIELD")
- @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
- public final void setTags(final String tags) {
- if (tags != null) {
- final String[] parts = tags.replace(", ", " ").replace(',', ' ').split(" ");
-
- SyndCategoryImpl tag;
- String part;
- char mod;
-
- for (final String rawPart : parts) {
- part = rawPart.trim();
-
- if (part.length() >= 2) {
- tag = new SyndCategoryImpl();
- tag.setName(part.substring(1).toLowerCase(Constants.LOCALE));
-
- mod = part.charAt(0);
-
- if (mod == '-') {
- // Don't remove the channel tag, if any
- if (!channel.substring(1).equals(tag.getName())) {
- this.tags.remove(tag);
- }
- } else if (mod == '+') {
- if (!this.tags.contains(tag)) {
- this.tags.add(tag);
- }
- } else {
- tag.setName(part.trim().toLowerCase(Constants.LOCALE));
-
- if (!this.tags.contains(tag)) {
- this.tags.add(tag);
- }
- }
- }
- }
- }
- }
-
- /**
- * Sets the tags.
- *
- * @param tags The tags.
- */
- final void setTags(final List tags) {
- this.tags.addAll(tags);
- }
-
- /**
- * Returns the comment's title.
- *
- * @return The title.
- */
- public final String getTitle() {
- return title;
- }
-
- /**
- * Sets the comment's title.
- *
- * @param title The new title.
- */
- public final void setTitle(final String title) {
- this.title = title;
- }
-
- /**
- * Returns true if the entry has comments.
- *
- * @return true if there are comments, false otherwise.
- */
- public final boolean hasComments() {
- return (!comments.isEmpty());
- }
-
- /**
- * Returns true if the entry has tags.
- *
- * @return true if there are tags, false otherwise.
- */
- public final boolean hasTags() {
- return (!tags.isEmpty());
- }
-
- /**
- * /** Sets a comment.
- *
- * @param index The comment's index.
- * @param comment The actual comment.
- * @param nick The nickname of the author of the comment.
- */
- public final void setComment(final int index, final String comment, final String nick) {
- if (index < comments.size()) {
- comments.set(index, new EntryComment(comment, nick));
- }
- }
-
- /**
- * Returns a string representation of the object.
- *
- * @return A string representation of the object.
- */
- @Override
- public final String toString() {
-
- return super.toString() + "[ channel -> '" + channel + '\'' + ", comments -> " + comments + ", date -> " + date
- + ", link -> '" + link + '\'' + ", login -> '" + login + '\'' + ", nick -> '" + nick + '\''
- + ", tags -> " + tags + ", title -> '" + title + '\'' + " ]";
- }
-}
diff --git a/src/main/java/net/thauvin/erik/mobibot/modules/AbstractModule.java b/src/main/java/net/thauvin/erik/mobibot/modules/AbstractModule.java
deleted file mode 100644
index 2d5409d..0000000
--- a/src/main/java/net/thauvin/erik/mobibot/modules/AbstractModule.java
+++ /dev/null
@@ -1,158 +0,0 @@
-/*
- * AbstractModule.java
- *
- * Copyright (c) 2004-2020, 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.modules;
-
-import net.thauvin.erik.mobibot.Mobibot;
-import org.apache.commons.lang3.StringUtils;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
-
-/**
- * The Module abstract class.
- *
- * @author Erik C. Thauvin
- * @created 2016-07-01
- * @since 1.0
- */
-public abstract class AbstractModule {
- final List commands = new ArrayList<>();
- final Map properties = new ConcurrentHashMap<>();
-
- /**
- * Responds to a command.
- *
- * @param bot The bot's instance.
- * @param sender The sender.
- * @param cmd The command.
- * @param args The command arguments.
- * @param isPrivate Set to true if the response should be sent as a private message.
- */
- public abstract void commandResponse(final Mobibot bot,
- final String sender,
- final String cmd,
- final String args,
- final boolean isPrivate);
-
- /**
- * Returns the module's commands, if any.
- *
- * @return The commands.
- */
- public List getCommands() {
- return commands;
- }
-
- /**
- * Returns the module's property keys.
- *
- * @return The keys.
- */
- public Set getPropertyKeys() {
- return properties.keySet();
- }
-
- /**
- * Returns true if the module has properties.
- *
- * @return true or false .
- */
- public boolean hasProperties() {
- return !properties.isEmpty();
- }
-
- /**
- * Responds with the module's help.
- *
- * @param bot The bot's instance.
- * @param sender The sender.
- * @param args The help arguments.
- * @param isPrivate Set to true if the response should be sent as a private message.
- */
- public abstract void helpResponse(final Mobibot bot,
- final String sender,
- final String args,
- @SuppressWarnings("unused") final boolean isPrivate);
-
- /**
- * Returns true if the module is enabled.
- *
- * @return true or false
- */
- public boolean isEnabled() {
- if (hasProperties()) {
- return isValidProperties();
- } else {
- return true;
- }
- }
-
- /**
- * Returns true if the module responds to private messages.
- *
- * @return true or false
- */
- public boolean isPrivateMsgEnabled() {
- return false;
- }
-
- /**
- * Ensures that all properties have values.
- *
- * @return true if the properties are valid, false otherwise.
- */
- boolean isValidProperties() {
- for (final String s : getPropertyKeys()) {
- if (StringUtils.isBlank(properties.get(s))) {
- return false;
- }
- }
-
- return true;
- }
-
- /**
- * Sets a property key and value.
- *
- * @param key The key.
- * @param value The value.
- */
- public void setProperty(final String key, final String value) {
- if (StringUtils.isNotBlank(key)) {
- properties.put(key, value);
- }
- }
-}
diff --git a/src/main/java/net/thauvin/erik/mobibot/modules/Calc.java b/src/main/java/net/thauvin/erik/mobibot/modules/Calc.java
deleted file mode 100644
index 6d4a304..0000000
--- a/src/main/java/net/thauvin/erik/mobibot/modules/Calc.java
+++ /dev/null
@@ -1,105 +0,0 @@
-/*
- * Calc.java
- *
- * Copyright (c) 2004-2020, 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.modules;
-
-import net.objecthunter.exp4j.Expression;
-import net.objecthunter.exp4j.ExpressionBuilder;
-import net.thauvin.erik.mobibot.Mobibot;
-import org.apache.commons.lang3.StringUtils;
-
-import java.text.DecimalFormat;
-
-/**
- * The Calc module.
- *
- * @author Erik C. Thauvin
- * @created 2016-07-01
- * @since 1.0
- */
-public class Calc extends AbstractModule {
- // Calc command
- private static final String CALC_CMD = "calc";
-
- /**
- * The default constructor.
- */
- public Calc() {
- super();
- commands.add(CALC_CMD);
- }
-
- /**
- * Performs a calculation.
- *
- *
1 + 1 * 2
- *
- * @param query The query.
- * @return The calculation result.
- */
- static String calc(final String query) {
- final DecimalFormat decimalFormat = new DecimalFormat("#.##");
-
- try {
- final Expression calc = new ExpressionBuilder(query).build();
- return query.replace(" ", "") + " = " + decimalFormat.format(calc.evaluate());
- } catch (Exception e) {
- return "No idea. This is the kind of math I don't get.";
- }
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void commandResponse(final Mobibot bot,
- final String sender,
- final String cmd,
- final String args,
- final boolean isPrivate) {
- if (StringUtils.isNotBlank(args)) {
- bot.send(calc(args));
-
- } else {
- helpResponse(bot, sender, args, isPrivate);
- }
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void helpResponse(final Mobibot bot, final String sender, final String args, final boolean isPrivate) {
- bot.send(sender, "To solve a mathematical calculation:");
- bot.send(sender, bot.helpIndent(bot.getNick() + ": " + CALC_CMD + " "));
- }
-}
diff --git a/src/main/java/net/thauvin/erik/mobibot/modules/CurrencyConverter.java b/src/main/java/net/thauvin/erik/mobibot/modules/CurrencyConverter.java
deleted file mode 100644
index a43f241..0000000
--- a/src/main/java/net/thauvin/erik/mobibot/modules/CurrencyConverter.java
+++ /dev/null
@@ -1,241 +0,0 @@
-/*
- * CurrencyConverter.java
- *
- * Copyright (c) 2004-2020, 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.modules;
-
-import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
-import net.thauvin.erik.mobibot.Constants;
-import net.thauvin.erik.mobibot.Mobibot;
-import net.thauvin.erik.mobibot.Utils;
-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.apache.commons.lang3.StringUtils;
-import org.jdom2.Document;
-import org.jdom2.Element;
-import org.jdom2.JDOMException;
-import org.jdom2.Namespace;
-import org.jdom2.input.SAXBuilder;
-
-import javax.xml.XMLConstants;
-import java.io.IOException;
-import java.net.URL;
-import java.text.NumberFormat;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.TreeMap;
-
-/**
- * The CurrentConverter module.
- *
- * @author Erik C. Thauvin
- * @created Feb 11, 2004
- * @since 1.0
- */
-@SuppressWarnings("PMD.UseConcurrentHashMap")
-public final class CurrencyConverter extends ThreadedModule {
- /**
- * The rates keyword.
- */
- static final String CURRENCY_RATES_KEYWORD = "rates";
-
- // Currency command
- private static final String CURRENCY_CMD = "currency";
- // Exchange rates
- private static final Map EXCHANGE_RATES = new TreeMap<>();
- // Exchange rates table URL
- private static final String EXCHANGE_TABLE_URL = "https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml";
- // Last exchange rates table publication date
- private static String pubDate = "";
-
- /**
- * Creates a new {@link CurrencyConverter} instance.
- */
- public CurrencyConverter() {
- super();
- commands.add(CURRENCY_CMD);
- }
-
- /**
- * Converts from a currency to another.
- *
- *
100 USD to EUR
- *
- * @param query The query.
- * @return The {@link Message} contained the converted currency.
- * @throws ModuleException If an error occurs while converting.
- */
- static Message convertCurrency(final String query) throws ModuleException {
- if (EXCHANGE_RATES.isEmpty()) {
- try {
- final SAXBuilder builder = new SAXBuilder();
- // See https://rules.sonarsourcecom/java/tag/owasp/RSPEC-2755
- builder.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "");
- builder.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
- builder.setIgnoringElementContentWhitespace(true);
-
- final Document doc = builder.build(new URL(EXCHANGE_TABLE_URL));
- final Element root = doc.getRootElement();
- final Namespace ns = root.getNamespace("");
- final Element cubeRoot = root.getChild("Cube", ns);
- final Element cubeTime = cubeRoot.getChild("Cube", ns);
-
- pubDate = cubeTime.getAttribute("time").getValue();
-
- final List cubes = cubeTime.getChildren();
- Element cube;
-
- for (final Element rawCube : cubes) {
- cube = rawCube;
- EXCHANGE_RATES.put(
- cube.getAttribute("currency").getValue(),
- cube.getAttribute("rate").getValue());
- }
-
- EXCHANGE_RATES.put("EUR", "1");
- } catch (JDOMException e) {
- throw new ModuleException(query, "An error has occurred while parsing the exchange rates table.", e);
- } catch (IOException e) {
- throw new ModuleException(
- query, "An error has occurred while fetching the exchange rates table.", e);
- }
- }
-
- if (EXCHANGE_RATES.isEmpty()) {
- return new ErrorMessage("Sorry, but the exchange rate table is empty.");
- } else {
- final String[] cmds = query.split(" ");
-
- if (cmds.length == 4) {
- if (cmds[3].equals(cmds[1]) || "0".equals(cmds[0])) {
- return new ErrorMessage("You're kidding, right?");
- } else {
- try {
- final double amt = Double.parseDouble(cmds[0].replace(",", ""));
- final double from =
- Double.parseDouble(EXCHANGE_RATES.get(cmds[1].toUpperCase(Constants.LOCALE)));
- final double to = Double.parseDouble(EXCHANGE_RATES.get(cmds[3].toUpperCase(Constants.LOCALE)));
-
- return new PublicMessage(
- NumberFormat.getCurrencyInstance(Locale.US).format(amt).substring(1)
- + ' '
- + cmds[1].toUpperCase(Constants.LOCALE)
- + " = "
- + NumberFormat.getCurrencyInstance(Locale.US)
- .format((amt * to) / from)
- .substring(1)
- + ' '
- + cmds[3].toUpperCase(Constants.LOCALE));
- } catch (Exception e) {
- throw new ModuleException("convertCurrency(" + query + ')',
- "The supported currencies are: " + EXCHANGE_RATES.keySet(), e);
- }
- }
- } else if (CURRENCY_RATES_KEYWORD.equals(query)) {
-
- final StringBuilder buff = new StringBuilder().append('[').append(pubDate).append("]: ");
-
- int i = 0;
- for (final Map.Entry rate : EXCHANGE_RATES.entrySet()) {
- if (i > 0) {
- buff.append(", ");
- }
- buff.append(rate.getKey()).append(": ").append(rate.getValue());
- i++;
- }
-
- return new NoticeMessage(buff.toString());
- }
- }
- return new ErrorMessage("The supported currencies are: " + EXCHANGE_RATES.keySet());
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void commandResponse(final Mobibot bot,
- final String sender,
- final String cmd,
- final String args,
- final boolean isPrivate) {
- synchronized (this) {
- if (!pubDate.equals(Utils.today())) {
- EXCHANGE_RATES.clear();
- }
- }
-
- super.commandResponse(bot, sender, cmd, args, isPrivate);
- }
-
- /**
- * Converts the specified currencies.
- */
- @SuppressFBWarnings("REDOS")
- @Override
- void run(final Mobibot bot, final String sender, final String cmd, final String query) {
- if (StringUtils.isNotBlank(sender) && StringUtils.isNotBlank(query)) {
- if (query.matches("\\d+([,\\d]+)?(\\.\\d+)? [a-zA-Z]{3}+ to [a-zA-Z]{3}+")) {
- try {
- final Message msg = convertCurrency(query);
- if (msg.isError()) {
- helpResponse(bot, sender, CURRENCY_CMD + ' ' + query, false);
- }
- bot.send(sender, msg);
- } catch (ModuleException e) {
- bot.getLogger().warn(e.getDebugMessage(), e);
- bot.send(sender, e.getMessage());
- }
- } else {
- helpResponse(bot, sender, CURRENCY_CMD + ' ' + query, true);
- }
- }
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void helpResponse(final Mobibot bot, final String sender, final String args, final boolean isPrivate) {
- bot.send(sender, "To convert from one currency to another:");
- bot.send(sender, bot.helpIndent(bot.getNick() + ": " + CURRENCY_CMD + " [100 USD to EUR]"));
-
- if (args.endsWith(CURRENCY_CMD)) {
- bot.send(sender, "For a listing of currency rates:");
- bot.send(sender, bot.helpIndent(bot.getNick() + ": " + CURRENCY_CMD) + ' ' + CURRENCY_RATES_KEYWORD);
- bot.send(sender, "For a listing of supported currencies:");
- bot.send(sender, bot.helpIndent(bot.getNick() + ": " + CURRENCY_CMD));
- }
- }
-}
diff --git a/src/main/java/net/thauvin/erik/mobibot/modules/Dice.java b/src/main/java/net/thauvin/erik/mobibot/modules/Dice.java
deleted file mode 100644
index 62b750c..0000000
--- a/src/main/java/net/thauvin/erik/mobibot/modules/Dice.java
+++ /dev/null
@@ -1,102 +0,0 @@
-/*
- * Dice.java
- *
- * Copyright (c) 2004-2020, 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.modules;
-
-import net.thauvin.erik.mobibot.Mobibot;
-import net.thauvin.erik.mobibot.Utils;
-
-import java.security.SecureRandom;
-
-/**
- * The Dice module.
- *
- * @author Erik C. Thauvin
- * @created 2014-04-28
- * @since 1.0
- */
-public final class Dice extends AbstractModule {
- // Dice command
- private static final String DICE_CMD = "dice";
-
- /**
- * The default constructor.
- */
- public Dice() {
- super();
- commands.add(DICE_CMD);
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void commandResponse(final Mobibot bot,
- final String sender,
- final String cmd,
- final String args,
- final boolean isPrivate) {
- final SecureRandom r = new SecureRandom();
-
- int i = r.nextInt(6) + 1;
- int y = r.nextInt(6) + 1;
- final int playerTotal = i + y;
-
- bot.send(bot.getChannel(),
- sender + " rolled two dice: " + Utils.bold(i) + " and " + Utils.bold(y) + " for a total of "
- + Utils.bold(playerTotal));
-
- i = r.nextInt(6) + 1;
- y = r.nextInt(6) + 1;
- final int total = i + y;
-
- bot.action(
- "rolled two dice: " + Utils.bold(i) + " and " + Utils.bold(y) + " for a total of " + Utils.bold(total));
-
- if (playerTotal < total) {
- bot.action("wins.");
- } else if (playerTotal > total) {
- bot.action("lost.");
- } else {
- bot.action("tied.");
- }
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void helpResponse(final Mobibot bot, final String sender, final String args, final boolean isPrivate) {
- bot.send(sender, "To roll the dice:");
- bot.send(sender, bot.helpIndent(bot.getNick() + ": " + DICE_CMD));
- }
-}
diff --git a/src/main/java/net/thauvin/erik/mobibot/modules/GoogleSearch.java b/src/main/java/net/thauvin/erik/mobibot/modules/GoogleSearch.java
deleted file mode 100644
index e180fb5..0000000
--- a/src/main/java/net/thauvin/erik/mobibot/modules/GoogleSearch.java
+++ /dev/null
@@ -1,175 +0,0 @@
-/*
- * GoogleSearch.java
- *
- * Copyright (c) 2004-2020, 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.modules;
-
-import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
-import net.thauvin.erik.mobibot.Mobibot;
-import net.thauvin.erik.mobibot.Utils;
-import net.thauvin.erik.mobibot.msg.Message;
-import net.thauvin.erik.mobibot.msg.NoticeMessage;
-import org.apache.commons.lang3.StringUtils;
-import org.jibble.pircbot.Colors;
-import org.json.JSONArray;
-import org.json.JSONObject;
-
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.net.URL;
-import java.net.URLConnection;
-import java.net.URLEncoder;
-import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * The GoogleSearch module.
- *
- * @author Erik C. Thauvin
- * @created Feb 7, 2004
- * @since 1.0
- */
-public final class GoogleSearch extends ThreadedModule {
- // Google API Key property
- static final String GOOGLE_API_KEY_PROP = "google-api-key";
- // Google Custom Search Engine ID property
- static final String GOOGLE_CSE_KEY_PROP = "google-cse-cx";
- // Google command
- private static final String GOOGLE_CMD = "google";
- // Tab indent (4 spaces)
- private static final String TAB_INDENT = " ";
-
- /**
- * Creates a new {@link GoogleSearch} instance.
- */
- public GoogleSearch() {
- super();
- commands.add(GOOGLE_CMD);
- properties.put(GOOGLE_API_KEY_PROP, "");
- properties.put(GOOGLE_CSE_KEY_PROP, "");
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void helpResponse(final Mobibot bot, final String sender, final String args, final boolean isPrivate) {
- if (isEnabled()) {
- bot.send(sender, "To search Google:");
- bot.send(sender, bot.helpIndent(bot.getNick() + ": " + GOOGLE_CMD + " "));
- } else {
- bot.send(sender, "The Google search module is disabled.");
- }
- }
-
- /**
- * Searches Google.
- */
- @Override
- void run(final Mobibot bot, final String sender, final String cmd, final String query) {
- if (StringUtils.isNotBlank(query)) {
- try {
- final List results = searchGoogle(query, properties.get(GOOGLE_API_KEY_PROP),
- properties.get(GOOGLE_CSE_KEY_PROP));
- for (final Message msg : results) {
- bot.send(sender, msg);
- }
- } catch (ModuleException e) {
- bot.getLogger().warn(e.getDebugMessage(), e);
- bot.send(sender, e.getMessage());
- }
- } else {
- helpResponse(bot, sender, query, true);
- }
- }
-
- /**
- * Performs a search on Google.
- *
- * @param query The search query.
- * @param apiKey The Google API key.
- * @param cseKey The Google CSE key.
- * @return The {@link Message} array containing the search results.
- * @throws ModuleException If an error occurs while searching.
- */
- @SuppressFBWarnings({"URLCONNECTION_SSRF_FD", "REC_CATCH_EXCEPTION"})
- @SuppressWarnings(("PMD.AvoidInstantiatingObjectsInLoops"))
- static List searchGoogle(final String query, final String apiKey, final String cseKey)
- throws ModuleException {
- if (StringUtils.isBlank(apiKey) || StringUtils.isBlank(cseKey)) {
- throw new ModuleException(StringUtils.capitalize(GOOGLE_CMD) + " is disabled. The API keys are missing.");
- }
-
- if (StringUtils.isNotBlank(query)) {
- final ArrayList results = new ArrayList<>();
- try {
- final String q = URLEncoder.encode(query, StandardCharsets.UTF_8.toString());
-
- final URL url =
- new URL("https://www.googleapis.com/customsearch/v1?key="
- + apiKey
- + "&cx="
- + cseKey
- + "&q="
- + q
- + "&filter=1&num=5&alt=json");
- final URLConnection conn = url.openConnection();
-
- final StringBuilder sb = new StringBuilder();
- try (final BufferedReader reader =
- new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
- String line;
- while ((line = reader.readLine()) != null) {
- sb.append(line);
- }
-
- final JSONObject json = new JSONObject(sb.toString());
- final JSONArray ja = json.getJSONArray("items");
-
- for (int i = 0; i < ja.length(); i++) {
- final JSONObject j = ja.getJSONObject(i);
- results.add(new NoticeMessage(Utils.unescapeXml(j.getString("title"))));
- results.add(
- new NoticeMessage(TAB_INDENT + j.getString("link"), Colors.DARK_GREEN));
- }
- }
- } catch (IOException e) {
- throw new ModuleException("searchGoogle(" + query + ')', "An error has occurred searching Google.", e);
- }
-
- return results;
- } else {
- throw new ModuleException("Invalid query.");
- }
- }
-}
diff --git a/src/main/java/net/thauvin/erik/mobibot/modules/Joke.java b/src/main/java/net/thauvin/erik/mobibot/modules/Joke.java
deleted file mode 100644
index 2210baf..0000000
--- a/src/main/java/net/thauvin/erik/mobibot/modules/Joke.java
+++ /dev/null
@@ -1,132 +0,0 @@
-/*
- * Joke.java
- *
- * Copyright (c) 2004-2020, 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.modules;
-
-import net.thauvin.erik.mobibot.Mobibot;
-import net.thauvin.erik.mobibot.Utils;
-import net.thauvin.erik.mobibot.msg.Message;
-import net.thauvin.erik.mobibot.msg.PublicMessage;
-import org.json.JSONObject;
-
-import java.io.BufferedReader;
-import java.io.InputStreamReader;
-import java.net.URL;
-import java.net.URLConnection;
-import java.nio.charset.StandardCharsets;
-
-/**
- * The Joke module.
- *
- * @author Erik C. Thauvin
- * @created 2014-04-20
- * @since 1.0
- */
-public final class Joke extends ThreadedModule {
- // Joke command
- private static final String JOKE_CMD = "joke";
- // ICNDB URL
- private static final String JOKE_URL =
- "http://api.icndb.com/jokes/random?escape=javascript&exclude=[explicit]&limitTo=[nerdy]";
-
- /**
- * Creates a new {@link Joke} instance.
- */
- public Joke() {
- super();
- commands.add(JOKE_CMD);
- }
-
- /**
- * Retrieves a random joke.
- *
- * @return The {@link Message} containing the new joke.
- * @throws ModuleException If an error occurs while retrieving a new joke.
- */
- static Message randomJoke() throws ModuleException {
- try {
- final URL url = new URL(JOKE_URL);
- final URLConnection conn = url.openConnection();
-
- final StringBuilder sb = new StringBuilder();
- try (final BufferedReader reader =
- new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
- String line;
- while ((line = reader.readLine()) != null) {
- sb.append(line);
- }
-
- final JSONObject json = new JSONObject(sb.toString());
-
- return new PublicMessage(
- json.getJSONObject("value").get("joke").toString().replace("\\'", "'")
- .replace("\\\"", "\""));
- }
- } catch (Exception e) {
- throw new ModuleException("randomJoke()", "An error has occurred retrieving a random joke.", e);
- }
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void commandResponse(final Mobibot bot,
- final String sender,
- final String cmd,
- final String args,
- final boolean isPrivate) {
- new Thread(() -> run(bot, sender, cmd, args)).start();
- }
-
- /**
- * Returns a random joke from The Internet Chuck Norris Database.
- */
- @Override
- void run(final Mobibot bot, final String sender, final String cmd, final String arg) {
- try {
- bot.send(Utils.cyan(randomJoke().getMessage()));
- } catch (ModuleException e) {
- bot.getLogger().warn(e.getDebugMessage(), e);
- bot.send(sender, e.getMessage());
- }
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void helpResponse(final Mobibot bot, final String sender, final String args, final boolean isPrivate) {
- bot.send(sender, "To retrieve a random joke:");
- bot.send(sender, bot.helpIndent(bot.getNick() + ": " + JOKE_CMD));
- }
-}
diff --git a/src/main/java/net/thauvin/erik/mobibot/modules/Lookup.java b/src/main/java/net/thauvin/erik/mobibot/modules/Lookup.java
deleted file mode 100644
index a3b808f..0000000
--- a/src/main/java/net/thauvin/erik/mobibot/modules/Lookup.java
+++ /dev/null
@@ -1,201 +0,0 @@
-/*
- * Lookup.java
- *
- * Copyright (c) 2004-2020, 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.modules;
-
-import net.thauvin.erik.mobibot.Constants;
-import net.thauvin.erik.mobibot.Mobibot;
-import org.apache.commons.net.whois.WhoisClient;
-
-import java.io.IOException;
-import java.net.InetAddress;
-import java.net.UnknownHostException;
-
-/**
- * The Lookup module.
- *
- * @author Erik C. Thauvin
- * @created 2014-04-26
- * @since 1.0
- */
-public final class Lookup extends AbstractModule {
- /**
- * The whois default host.
- */
- static final String WHOIS_HOST = "whois.arin.net";
-
- // Lookup command
- private static final String LOOKUP_CMD = "lookup";
-
- /**
- * The default constructor.
- */
- public Lookup() {
- super();
- commands.add(LOOKUP_CMD);
- }
-
- /**
- * Performs a DNS lookup on the specified query.
- *
- * @param query The IP address or hostname.
- * @return The lookup query result string.
- * @throws java.net.UnknownHostException If the host is unknown.
- */
- public static String lookup(final String query)
- throws UnknownHostException {
- final StringBuilder buffer = new StringBuilder();
-
- final InetAddress[] results = InetAddress.getAllByName(query);
- String hostInfo;
-
- for (final InetAddress result : results) {
- if (result.getHostAddress().equals(query)) {
- hostInfo = result.getHostName();
-
- if (hostInfo.equals(query)) {
- throw new UnknownHostException();
- }
- } else {
- hostInfo = result.getHostAddress();
- }
-
- if (buffer.length() > 0) {
- buffer.append(", ");
- }
-
- buffer.append(hostInfo);
- }
-
- return buffer.toString();
- }
-
- /**
- * Performs a whois IP query.
- *
- * @param query The IP address.
- * @return The IP whois data, if any.
- * @throws java.io.IOException If a connection error occurs.
- */
- private static String[] whois(final String query)
- throws IOException {
- return whois(query, WHOIS_HOST);
- }
-
- /**
- * Performs a whois IP query.
- *
- * @param query The IP address.
- * @param host The whois host.
- * @return The IP whois data, if any.
- * @throws java.io.IOException If a connection error occurs.
- */
- public static String[] whois(final String query, final String host)
- throws IOException {
- final WhoisClient whoisClient = new WhoisClient();
- final String[] lines;
-
- try {
- whoisClient.setDefaultTimeout(Constants.CONNECT_TIMEOUT);
- whoisClient.connect(host);
- whoisClient.setSoTimeout(Constants.CONNECT_TIMEOUT);
- whoisClient.setSoLinger(false, 0);
-
- if (WHOIS_HOST.equals(host)) {
- lines = whoisClient.query("n - " + query).split("\n");
- } else {
- lines = whoisClient.query(query).split("\n");
- }
- } finally {
- whoisClient.disconnect();
- }
-
- return lines;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void commandResponse(final Mobibot bot,
- final String sender,
- final String cmd,
- final String args,
- final boolean isPrivate) {
- if (args.matches("(\\S.)+(\\S)+")) {
- try {
- bot.send(Lookup.lookup(args));
- } catch (UnknownHostException ignore) {
- 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]?)")) {
- try {
- final String[] lines = Lookup.whois(args);
-
- if ((lines != null) && (lines.length > 0)) {
- String line;
-
- for (final String rawLine : lines) {
- line = rawLine.trim();
-
- if ((line.length() > 0) && (line.charAt(0) != '#')) {
- bot.send(line);
- }
- }
- } else {
- bot.send("Unknown host.");
- }
- } catch (IOException ioe) {
- if (bot.getLogger().isDebugEnabled()) {
- bot.getLogger().debug("Unable to perform whois IP lookup: {}", args, ioe);
- }
-
- bot.send("Unable to perform whois IP lookup: " + ioe.getMessage());
- }
- } else {
- bot.send("Unknown host.");
- }
- }
- } else {
- helpResponse(bot, sender, args, true);
- }
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void helpResponse(final Mobibot bot, final String sender, final String args, final boolean isPrivate) {
- bot.send(sender, "To perform a DNS lookup query:");
- bot.send(sender, bot.helpIndent(bot.getNick() + ": " + LOOKUP_CMD + " "));
- }
-}
diff --git a/src/main/java/net/thauvin/erik/mobibot/modules/ModuleException.java b/src/main/java/net/thauvin/erik/mobibot/modules/ModuleException.java
deleted file mode 100644
index 13ab2fa..0000000
--- a/src/main/java/net/thauvin/erik/mobibot/modules/ModuleException.java
+++ /dev/null
@@ -1,117 +0,0 @@
-/*
- * ModuleException.java
- *
- * Copyright (c) 2004-2019, 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.modules;
-
-import net.thauvin.erik.mobibot.Utils;
-import org.apache.commons.lang3.StringUtils;
-
-/**
- * The ModuleException class.
- *
- * @author Erik C. Thauvin
- * @created 2019-04-07
- * @since 1.0
- */
-public class ModuleException extends Exception {
- private static final long serialVersionUID = 1L;
-
- private final String debugMessage;
-
- /**
- * Creates a new exception.
- *
- * @param message The exception message.
- */
- ModuleException(final String message) {
- super(message);
- this.debugMessage = message;
- }
-
- /**
- * Creates a new exception.
- *
- * @param debugMessage The debug message.
- * @param message The exception message.
- * @param cause The cause.
- */
- ModuleException(final String debugMessage, final String message, final Throwable cause) {
- super(message, cause);
- this.debugMessage = debugMessage;
- }
-
- /**
- * Creates a new exception.
- *
- * @param debugMessage The debug message.
- * @param message The exception message.
- */
- ModuleException(final String debugMessage, final String message) {
- super(message);
- this.debugMessage = debugMessage;
- }
-
- /**
- * Returns the debug message.
- *
- * @return The debug message.
- */
- String getDebugMessage() {
- return debugMessage;
- }
-
- /**
- * Return the sanitized message (e.g. remove API keys, etc.)
- *
- * @param sanitize The words to sanitize.
- * @return The sanitized message.
- */
- String getSanitizedMessage(final String... sanitize) {
- final String[] obfuscate = new String[sanitize.length];
- for (int i = 0; i < sanitize.length; i++) {
- obfuscate[i] = Utils.obfuscate(sanitize[i]);
- }
- return getCause().getClass().getName() + ": " + StringUtils.replaceEach(getCause().getMessage(),
- sanitize,
- obfuscate);
- }
-
- /**
- * Return true if the exception has a cause.
- *
- * @return true or false
- */
- @SuppressWarnings("unused")
- boolean hasCause() {
- return getCause() != null;
- }
-}
diff --git a/src/main/java/net/thauvin/erik/mobibot/modules/Ping.java b/src/main/java/net/thauvin/erik/mobibot/modules/Ping.java
deleted file mode 100644
index 214a1ed..0000000
--- a/src/main/java/net/thauvin/erik/mobibot/modules/Ping.java
+++ /dev/null
@@ -1,100 +0,0 @@
-/*
- * Ping.java
- *
- * Copyright (c) 2004-2020, 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.modules;
-
-import net.thauvin.erik.mobibot.Mobibot;
-
-import java.security.SecureRandom;
-import java.util.Arrays;
-import java.util.List;
-
-/**
- * The Ping module.
- *
- * @author Erik C. Thauvin
- * @created 2016-07-02
- * @since 1.0
- */
-public class Ping extends AbstractModule {
- /**
- * The ping responses.
- */
- static final List PINGS =
- Arrays.asList(
- "is barely alive.",
- "is trying to stay awake.",
- "has gone fishing.",
- "is somewhere over the rainbow.",
- "has fallen and can't get up.",
- "is running. You better go chase it.",
- "has just spontaneously combusted.",
- "is talking to itself... don't interrupt. That's rude.",
- "is bartending at an AA meeting.",
- "is hibernating.",
- "is saving energy: apathetic mode activated.",
- "is busy. Go away!");
- /**
- * The ping command.
- */
- private static final String PING_CMD = "ping";
-
- /**
- * The default constructor.
- */
- public Ping() {
- super();
- commands.add(PING_CMD);
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void commandResponse(final Mobibot bot,
- final String sender,
- final String cmd,
- final String args,
- final boolean isPrivate) {
- final SecureRandom r = new SecureRandom();
- bot.action(PINGS.get(r.nextInt(PINGS.size())));
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void helpResponse(final Mobibot bot, final String sender, final String args, final boolean isPrivate) {
- bot.send(sender, "To ping the bot:");
- bot.send(sender, bot.helpIndent(bot.getNick() + ": " + PING_CMD));
- }
-}
diff --git a/src/main/java/net/thauvin/erik/mobibot/modules/StockQuote.java b/src/main/java/net/thauvin/erik/mobibot/modules/StockQuote.java
deleted file mode 100644
index 42c5057..0000000
--- a/src/main/java/net/thauvin/erik/mobibot/modules/StockQuote.java
+++ /dev/null
@@ -1,229 +0,0 @@
-/*
- * StockQuote.java
- *
- * Copyright (c) 2004-2020, 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.modules;
-
-import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
-import net.thauvin.erik.mobibot.Mobibot;
-import net.thauvin.erik.mobibot.Utils;
-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 okhttp3.OkHttpClient;
-import okhttp3.Request;
-import okhttp3.Response;
-import org.apache.commons.lang3.StringUtils;
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Objects;
-
-/**
- * The StockQuote module.
- *
- * @author Erik C. Thauvin
- * @created Feb 7, 2004
- * @since 1.0
- */
-public final class StockQuote extends ThreadedModule {
- /**
- * The Alpha Advantage property key.
- */
- static final String ALPHAVANTAGE_API_KEY_PROP = "alphavantage-api-key";
- /**
- * The Invalid Symbol error string.
- */
- static final String INVALID_SYMBOL = "Invalid symbol.";
- // Alpha Advantage URL
- private static final String ALAPHAVANTAGE_URL = "https://www.alphavantage.co/query?function=";
- // Quote command
- private static final String STOCK_CMD = "stock";
-
- /**
- * Creates a new {@link StockQuote} instance.
- */
- public StockQuote() {
- super();
- commands.add(STOCK_CMD);
- properties.put(ALPHAVANTAGE_API_KEY_PROP, "");
- }
-
- @SuppressFBWarnings(value = "NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE", justification = "false positive?")
- private static JSONObject getJsonResponse(final Response response, final String debugMessage)
- throws IOException, ModuleException {
- if (response.isSuccessful()) {
- if (response.body() != null) {
- final JSONObject json = new JSONObject(Objects.requireNonNull(response.body()).string());
-
- try {
- final String info = json.getString("Information");
- if (!info.isEmpty()) {
- throw new ModuleException(debugMessage, Utils.unescapeXml(info));
- }
- } catch (JSONException ignore) {
- // Do nothing
- }
-
- try {
- final String error = json.getString("Note");
- if (!error.isEmpty()) {
- throw new ModuleException(debugMessage, Utils.unescapeXml(error));
- }
- } catch (JSONException ignore) {
- // Do nothing
- }
-
- try {
- final String error = json.getString("Error Message");
- if (!error.isEmpty()) {
- throw new ModuleException(debugMessage, Utils.unescapeXml(error));
- }
- } catch (JSONException ignore) {
- // Do nothing
- }
-
- return json;
- } else {
- throw new ModuleException(debugMessage, "Invalid Response (" + response.code() + ')');
- }
- } else {
- throw new ModuleException(debugMessage, "Empty Response.");
- }
- }
-
- /**
- * Retrieves a stock quote.
- *
- * @param symbol The stock symbol.
- * @return The {@link Message} array containing the stock quote.
- * @throws ModuleException If an errors occurs.
- */
- @SuppressWarnings({"PMD.CloseResource"})
- static List getQuote(final String symbol, final String apiKey) throws ModuleException {
- if (StringUtils.isBlank(apiKey)) {
- throw new ModuleException(StringUtils.capitalize(STOCK_CMD) + " is disabled. The API key is missing.");
- }
-
- if (StringUtils.isNotBlank(symbol)) {
- final String debugMessage = "getQuote(" + symbol + ')';
- final ArrayList messages = new ArrayList<>();
- final OkHttpClient client = new OkHttpClient();
-
- try {
- // Search for symbol/keywords
- Request request = new Request.Builder().url(
- ALAPHAVANTAGE_URL + "SYMBOL_SEARCH&keywords=" + symbol + "&apikey=" + apiKey).build();
- Response response = client.newCall(request).execute();
-
- JSONObject json = getJsonResponse(response, debugMessage);
-
- final JSONArray symbols = json.getJSONArray("bestMatches");
- if (symbols.isEmpty()) {
- messages.add(new ErrorMessage(INVALID_SYMBOL));
- return messages;
- }
-
- final JSONObject symbolInfo = symbols.getJSONObject(0);
-
- // Get quote for symbol
- request = new Request.Builder().url(
- ALAPHAVANTAGE_URL + "GLOBAL_QUOTE&symbol=" + symbolInfo.getString("1. symbol") + "&apikey="
- + apiKey).build();
- response = client.newCall(request).execute();
-
- json = getJsonResponse(response, debugMessage);
-
- final JSONObject quote = json.getJSONObject("Global Quote");
-
- if (quote.isEmpty()) {
- messages.add(new ErrorMessage(INVALID_SYMBOL));
- return messages;
- }
-
- messages.add(new PublicMessage(
- "Symbol: " + Utils.unescapeXml(quote.getString("01. symbol")) + " [" + Utils
- .unescapeXml(symbolInfo.getString("2. name") + ']')));
- messages.add(new PublicMessage(" Price: " + Utils.unescapeXml(quote.getString("05. price"))));
- messages.add(
- new PublicMessage(" Previous: " + Utils.unescapeXml(quote.getString("08. previous close"))));
- messages.add(new NoticeMessage(" Open: " + Utils.unescapeXml(quote.getString("02. open"))));
- messages.add(new NoticeMessage(" High: " + Utils.unescapeXml(quote.getString("03. high"))));
- messages.add(new NoticeMessage(" Low: " + Utils.unescapeXml(quote.getString("04. low"))));
- messages.add(new NoticeMessage(" Volume: " + Utils.unescapeXml(quote.getString("06. volume"))));
- messages.add(new NoticeMessage(
- " Latest: " + Utils.unescapeXml(quote.getString("07. latest trading day"))));
- messages.add(new NoticeMessage(
- " Change: " + Utils.unescapeXml(quote.getString("09. change")) + " [" + Utils
- .unescapeXml(quote.getString("10. change percent")) + ']'));
- } catch (IOException | NullPointerException e) {
- throw new ModuleException(debugMessage, "An error has occurred retrieving a stock quote.", e);
- }
- return messages;
- } else {
- throw new ModuleException(INVALID_SYMBOL);
- }
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void helpResponse(final Mobibot bot, final String sender, final String args, final boolean isPrivate) {
- bot.send(sender, "To retrieve a stock quote:");
- bot.send(sender, bot.helpIndent(bot.getNick() + ": " + STOCK_CMD + " "));
- }
-
- /**
- * Returns the specified stock quote from Alpha Advantage.
- */
- @Override
- void run(final Mobibot bot, final String sender, final String cmd, final String symbol) {
- if (StringUtils.isNotBlank(symbol)) {
- try {
- final List messages = getQuote(symbol, properties.get(ALPHAVANTAGE_API_KEY_PROP));
- for (final Message msg : messages) {
- bot.send(sender, msg);
- }
- } catch (ModuleException e) {
- bot.getLogger().warn(e.getDebugMessage(), e);
- bot.send(e.getMessage());
- }
- } else {
- helpResponse(bot, sender, symbol, true);
- }
- }
-}
diff --git a/src/main/java/net/thauvin/erik/mobibot/modules/Twitter.java b/src/main/java/net/thauvin/erik/mobibot/modules/Twitter.java
deleted file mode 100644
index 1b7af6e..0000000
--- a/src/main/java/net/thauvin/erik/mobibot/modules/Twitter.java
+++ /dev/null
@@ -1,158 +0,0 @@
-/*
- * Twitter.java
- *
- * Copyright (c) 2004-2020, 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.modules;
-
-import net.thauvin.erik.mobibot.Mobibot;
-import net.thauvin.erik.mobibot.msg.Message;
-import net.thauvin.erik.mobibot.msg.NoticeMessage;
-import twitter4j.DirectMessage;
-import twitter4j.Status;
-import twitter4j.TwitterFactory;
-import twitter4j.conf.ConfigurationBuilder;
-
-/**
- * The Twitter module.
- *
- * @author Erik C. Thauvin
- * @created Sept 10, 2008
- * @since 1.0
- */
-public final class Twitter extends ThreadedModule {
- // Property keys
- static final String CONSUMER_KEY_PROP = "twitter-consumerKey";
- static final String CONSUMER_SECRET_PROP = "twitter-consumerSecret";
- static final String TOKEN_PROP = "twitter-token";
- static final String TOKEN_SECRET_PROP = "twitter-tokenSecret";
- // Twitter command
- private static final String TWITTER_CMD = "twitter";
-
- /**
- * Creates a new {@link Twitter} instance.
- */
- public Twitter() {
- super();
- commands.add(TWITTER_CMD);
- properties.put(CONSUMER_SECRET_PROP, "");
- properties.put(CONSUMER_KEY_PROP, "");
- properties.put(TOKEN_PROP, "");
- properties.put(TOKEN_SECRET_PROP, "");
- }
-
- /**
- * Posts on Twitter.
- *
- * @param consumerKey The consumer key.
- * @param consumerSecret The consumer secret.
- * @param token The token.
- * @param tokenSecret The token secret.
- * @param handle The Twitter handle (dm) or nickname.
- * @param message The message to post.
- * @param isDm The direct message flag.
- * @return The confirmation {@link Message}.
- * @throws ModuleException If an error occurs while posting.
- */
- static Message twitterPost(final String consumerKey,
- final String consumerSecret,
- final String token,
- final String tokenSecret,
- final String handle,
- final String message,
- final boolean isDm) throws ModuleException {
- try {
- final ConfigurationBuilder cb = new ConfigurationBuilder();
- cb.setDebugEnabled(true).setOAuthConsumerKey(consumerKey).setOAuthConsumerSecret(consumerSecret)
- .setOAuthAccessToken(token).setOAuthAccessTokenSecret(tokenSecret);
-
- final TwitterFactory tf = new TwitterFactory(cb.build());
- final twitter4j.Twitter twitter = tf.getInstance();
-
- if (!isDm) {
- final Status status = twitter.updateStatus(message);
- return new NoticeMessage("You message was posted to https://twitter.com/" + twitter.getScreenName()
- + "/statuses/" + status.getId());
- } else {
- final DirectMessage dm = twitter.sendDirectMessage(handle, message);
- return new NoticeMessage(dm.getText());
- }
- } catch (Exception e) {
- throw new ModuleException("twitterPost(" + message + ")", "An error has occurred: " + e.getMessage(), e);
- }
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void helpResponse(final Mobibot bot, final String sender, final String args, final boolean isPrivate) {
- if (isEnabled()) {
- bot.send(sender, "To post to Twitter:");
- bot.send(sender, bot.helpIndent(bot.getNick() + ": " + TWITTER_CMD + " "));
- } else {
- bot.send(sender, "The Twitter posting facility is disabled.");
- }
- }
-
- /**
- * Posts on Twitter.
- *
- * @param handle The Twitter handle (dm) or nickname.
- * @param message The message to post.
- * @param isDm The direct message flag.
- * @return The {@link Message} to send back.
- * @throws ModuleException If an error occurs while posting.
- */
- public Message post(final String handle, final String message, final boolean isDm)
- throws ModuleException {
- return twitterPost(properties.get(CONSUMER_KEY_PROP),
- properties.get(CONSUMER_SECRET_PROP),
- properties.get(TOKEN_PROP),
- properties.get(TOKEN_SECRET_PROP),
- handle,
- message,
- isDm);
- }
-
- /**
- * Posts to twitter.
- */
- @Override
- void run(final Mobibot bot, final String sender, final String cmd, final String message) {
- try {
- bot.send(sender,
- post(sender, message + " (by " + sender + " on " + bot.getChannel() + ')', false).getMessage());
- } catch (ModuleException e) {
- bot.getLogger().warn(e.getDebugMessage(), e);
- bot.send(sender, e.getMessage());
- }
- }
-}
diff --git a/src/main/java/net/thauvin/erik/mobibot/modules/War.java b/src/main/java/net/thauvin/erik/mobibot/modules/War.java
index 115e6ef..5565ecb 100644
--- a/src/main/java/net/thauvin/erik/mobibot/modules/War.java
+++ b/src/main/java/net/thauvin/erik/mobibot/modules/War.java
@@ -1,7 +1,7 @@
/*
* War.java
*
- * Copyright (c) 2004-2020, Erik C. Thauvin (erik@thauvin.net)
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -32,77 +32,82 @@
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;
+import static net.thauvin.erik.mobibot.Utils.bold;
+
/**
* The War module.
*
* @author Erik C. Thauvin
- * @created 2014-04-28
* @since 1.0
*/
public final class War extends AbstractModule {
+ // Random
+ private static final SecureRandom RANDOM = new SecureRandom();
// War command
private static final String WAR_CMD = "war";
- // Deck of card
- private static final String[] WAR_DECK =
- new String[]{ "Ace", "King", "Queen", "Jack", "10", "9", "8", "7", "6", "5", "4", "3", "2" };
- // Suits for the deck of card
- private static final String[] WAR_SUITS = new String[]{ "Hearts", "Spades", "Diamonds", "Clubs" };
+
+ private static final String[] HEARTS =
+ {"🂱", "🂾", "🂽", "🂼", "🂻", "🂺", "🂹", "🂸", "🂷", "🂶", "🂵", "🂴", "🂳", "🂲"};
+ private static final String[] SPADES =
+ {"🂡", "🂮", "🂭", "🂬", "🂫", "🂪", "🂩", "🂨", "🂧", "🂦", "🂥", "🂤", "🂣", "🂢"};
+ private static final String[] DIAMONDS =
+ {"🃁", "🃎", "🃍", "🃌", "🃋", "🃊", "🃉", "🃈", "🃇", "🃆", "🃅", "🃄", "🃃", "🃂"};
+ private static final String[] CLUBS =
+ {"🃑", "🃞", "🃝", "🃜", "🃛", "🃚", "🃙", "🃘", "🃗", "🃖", "🃕", "🃔", "🃓", "🃒"};
+
+ private static final String[][] DECK = {HEARTS, SPADES, DIAMONDS, CLUBS};
/**
* The default constructor.
*/
public War() {
super();
+
commands.add(WAR_CMD);
+
+ help.add("To play war:");
+ help.add(Utils.helpFormat("%c " + WAR_CMD));
+ }
+
+ @NotNull
+ @Override
+ public String getName() {
+ return "War";
}
/**
* {@inheritDoc}
*/
@Override
- public void commandResponse(final Mobibot bot,
- final String sender,
- final String cmd,
- final String args,
- final boolean isPrivate) {
- final SecureRandom r = new SecureRandom();
-
+ public void commandResponse(@NotNull final String channel, @NotNull final String cmd, @NotNull final String args,
+ @NotNull final GenericMessageEvent event) {
int i;
int y;
while (true) {
- i = r.nextInt(WAR_DECK.length);
- y = r.nextInt(WAR_DECK.length);
+ i = RANDOM.nextInt(HEARTS.length);
+ y = RANDOM.nextInt(HEARTS.length);
- bot.send(bot.getChannel(),
- sender + " drew the " + Utils.bold(WAR_DECK[i]) + " of " + WAR_SUITS[r.nextInt(WAR_SUITS.length)]);
- bot.action("drew the " + Utils.bold(WAR_DECK[y]) + " of " + WAR_SUITS[r.nextInt(WAR_SUITS.length)]);
+ 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;
}
- bot.send("This means " + Utils.bold("WAR") + '!');
+ event.respond("This means " + bold("WAR") + '!');
}
if (i < y) {
- bot.action("lost.");
+ event.getBot().sendIRC().action(channel, "lost.");
} else {
- bot.action("wins.");
+ event.getBot().sendIRC().action(channel, "wins.");
}
}
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void helpResponse(final Mobibot bot, final String sender, final String args, final boolean isPrivate) {
- bot.send(sender, "To play war:");
- bot.send(sender, bot.helpIndent(bot.getNick() + ": " + WAR_CMD));
- }
}
diff --git a/src/main/java/net/thauvin/erik/mobibot/modules/Weather2.java b/src/main/java/net/thauvin/erik/mobibot/modules/Weather2.java
deleted file mode 100644
index 5f1a376..0000000
--- a/src/main/java/net/thauvin/erik/mobibot/modules/Weather2.java
+++ /dev/null
@@ -1,244 +0,0 @@
-/*
- * Weather2.java
- *
- * Copyright (c) 2004-2020, 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.modules;
-
-import net.aksingh.owmjapis.api.APIException;
-import net.aksingh.owmjapis.core.OWM;
-import net.aksingh.owmjapis.model.CurrentWeather;
-import net.aksingh.owmjapis.model.param.Main;
-import net.aksingh.owmjapis.model.param.Weather;
-import net.aksingh.owmjapis.model.param.Wind;
-import net.thauvin.erik.mobibot.Mobibot;
-import net.thauvin.erik.mobibot.Utils;
-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 okhttp3.HttpUrl;
-import org.apache.commons.lang3.StringUtils;
-import org.jibble.pircbot.Colors;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Objects;
-
-/**
- * The Weather2 module.
- *
- * @author Erik C. Thauvin
- * @created 2017-04-02
- * @since 1.0
- */
-public class Weather2 extends ThreadedModule {
- /**
- * The OpenWeatherMap API Key property.
- */
- static final String OWM_API_KEY_PROP = "owm-api-key";
-
- // Weather command
- private static final String WEATHER_CMD = "weather";
-
- /**
- * Creates a new {@link Weather2} instance.
- */
- public Weather2() {
- super();
- commands.add(WEATHER_CMD);
- properties.put(OWM_API_KEY_PROP, "");
- }
-
- private static OWM.Country getCountry(final String countryCode) {
- for (final OWM.Country c : OWM.Country.values()) {
- if (c.name().equalsIgnoreCase(countryCode)) {
- return c;
- }
- }
-
- return OWM.Country.UNITED_STATES;
- }
-
- @SuppressWarnings("AvoidEscapedUnicodeCharacters")
- private static String getTemps(final Double d) {
- final double c = (d - 32) * 5 / 9;
- return Math.round(d) + " \u00B0F, " + Math.round(c) + " \u00B0C";
- }
-
- /**
- * Retrieves the weather data.
- *
- *
- *
98204
- *
London, UK
- *
- *
- * @param query The query.
- * @param apiKey The API key.
- * @return The {@link Message} array containing the weather data.
- * @throws ModuleException If an error occurs while retrieving the weather data.
- */
- static List getWeather(final String query, final String apiKey) throws ModuleException {
- if (StringUtils.isBlank(apiKey)) {
- throw new ModuleException(StringUtils.capitalize(WEATHER_CMD) + " is disabled. The API key is missing.");
- }
-
- final OWM owm = new OWM(apiKey);
- final ArrayList messages = new ArrayList<>();
-
- owm.setUnit(OWM.Unit.IMPERIAL);
-
- if (StringUtils.isNotBlank(query)) {
- final String[] argv = query.split(",");
-
- if (argv.length >= 1 && argv.length <= 2) {
- final String country;
- final String city = argv[0].trim();
- if (argv.length > 1 && StringUtils.isNotBlank(argv[1])) {
- country = argv[1].trim();
- } else {
- country = "US";
- }
-
- try {
- final CurrentWeather cwd;
- if (city.matches("\\d+")) {
- cwd = owm.currentWeatherByZipCode(Integer.parseInt(city), getCountry(country));
- } else {
- cwd = owm.currentWeatherByCityName(city, getCountry(country));
- }
- if (cwd.hasCityName()) {
- messages.add(new PublicMessage("City: " + cwd.getCityName() + " [" + country + "]"));
-
- final Main main = cwd.getMainData();
- if (main != null) {
- if (main.hasTemp()) {
- messages.add(new PublicMessage("Temperature: " + getTemps(main.getTemp())));
- }
-
- if (main.hasHumidity() && (main.getHumidity() != null)) {
- messages.add(new NoticeMessage("Humidity: " + Math.round(main.getHumidity()) + "%"));
- }
- }
-
- if (cwd.hasWindData()) {
- final Wind w = cwd.getWindData();
- if (w != null && w.hasSpeed()) {
- messages.add(new NoticeMessage("Wind: " + wind(w.getSpeed())));
- }
- }
-
- if (cwd.hasWeatherList()) {
- final StringBuilder condition = new StringBuilder("Condition: ");
- final List list = cwd.getWeatherList();
- if (list != null) {
- for (final Weather w : list) {
- if (condition.indexOf(",") == -1) {
- condition.append(StringUtils.capitalize(w.getDescription()));
- } else {
- condition.append(", ").append(w.getDescription());
- }
- }
- messages.add(new NoticeMessage(condition.toString()));
- }
- }
-
- if (cwd.hasCityId() && cwd.getCityId() != null) {
- if (cwd.getCityId() > 0) {
- messages.add(new NoticeMessage("https://openweathermap.org/city/" + cwd.getCityId(),
- Colors.GREEN));
- } else {
- final HttpUrl url = Objects.requireNonNull(HttpUrl.parse(
- "https://openweathermap.org/find"))
- .newBuilder()
- .addQueryParameter("q",
- city + ',' + country)
- .build();
- messages.add(new NoticeMessage(url.toString(), Colors.GREEN));
- }
- }
- }
- } catch (APIException | NullPointerException e) {
- throw new ModuleException("getWeather(" + query + ')', "Unable to perform weather lookup.", e);
- }
- }
- }
-
- if (messages.isEmpty()) {
- messages.add(new ErrorMessage("Invalid syntax."));
- }
-
- return messages;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void helpResponse(final Mobibot bot, final String sender, final String args, final boolean isPrivate) {
- bot.send(sender, "To display weather information:");
- bot.send(sender, bot.helpIndent(bot.getNick() + ": " + WEATHER_CMD + " [, ]"));
- bot.send(sender, "For example:");
- bot.send(sender, bot.helpIndent(bot.getNick() + ": " + WEATHER_CMD + " paris, fr"));
- bot.send(sender,
- "The default ISO 3166 country code is " + Utils.bold("US")
- + ". Zip codes are supported in most countries.");
- }
-
- /**
- * Fetches the weather data from a specific city.
- */
- @Override
- void run(final Mobibot bot, final String sender, final String cmd, final String args) {
- if (StringUtils.isNotBlank(args)) {
- try {
- final List messages = getWeather(args, properties.get(OWM_API_KEY_PROP));
- if (messages.get(0).isError()) {
- helpResponse(bot, sender, args, true);
- } else {
- for (final Message msg : messages) {
- bot.send(sender, msg);
- }
- }
- } catch (ModuleException e) {
- bot.getLogger().debug(e.getDebugMessage(), e);
- bot.send(e.getMessage());
- }
- } else {
- helpResponse(bot, sender, args, true);
- }
- }
-
- private static String wind(final Double w) {
- final double kmh = w * 1.60934;
- return Math.round(w) + " mph, " + Math.round(kmh) + " km/h";
- }
-}
diff --git a/src/main/java/net/thauvin/erik/mobibot/modules/WorldTime.java b/src/main/java/net/thauvin/erik/mobibot/modules/WorldTime.java
deleted file mode 100644
index 7bbc09d..0000000
--- a/src/main/java/net/thauvin/erik/mobibot/modules/WorldTime.java
+++ /dev/null
@@ -1,249 +0,0 @@
-/*
- * WorldTime.java
- *
- * Copyright (c) 2004-2020, 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.modules;
-
-import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
-import net.thauvin.erik.mobibot.Constants;
-import net.thauvin.erik.mobibot.Mobibot;
-import net.thauvin.erik.mobibot.msg.ErrorMessage;
-import net.thauvin.erik.mobibot.msg.Message;
-import net.thauvin.erik.mobibot.msg.PublicMessage;
-
-import java.time.ZoneId;
-import java.time.ZonedDateTime;
-import java.time.format.DateTimeFormatter;
-import java.time.temporal.ChronoField;
-import java.util.Collections;
-import java.util.Map;
-import java.util.TreeMap;
-
-/**
- * The WorldTime module.
- *
- * @author Erik C. Thauvin
- * @created 2014-04-27
- * @since 1.0
- */
-@SuppressWarnings("PMD.UseConcurrentHashMap")
-public final class WorldTime extends AbstractModule {
- // Beats (Internet Time) keyword
- private static final String BEATS_KEYWORD = ".beats";
- // Supported countries
- private static final Map COUNTRIES_MAP;
-
-
- /**
- * The time command.
- */
- private static final String TIME_CMD = "time";
-
- static {
- // Initialize the countries map
- final Map countries = new TreeMap<>();
- countries.put("AE", "Asia/Dubai");
- countries.put("AF", "Asia/Kabul");
- countries.put("AQ", "Antarctica/South_Pole");
- countries.put("AT", "Europe/Vienna");
- countries.put("AU", "Australia/Sydney");
- countries.put("AKST", "America/Anchorage");
- countries.put("AKDT", "America/Anchorage");
- countries.put("BE", "Europe/Brussels");
- countries.put("BR", "America/Sao_Paulo");
- countries.put("CA", "America/Montreal");
- countries.put("CDT", "America/Chicago");
- countries.put("CET", "CET");
- countries.put("CH", "Europe/Zurich");
- countries.put("CN", "Asia/Shanghai");
- countries.put("CST", "America/Chicago");
- countries.put("CU", "Cuba");
- countries.put("DE", "Europe/Berlin");
- countries.put("DK", "Europe/Copenhagen");
- countries.put("EDT", "America/New_York");
- countries.put("EG", "Africa/Cairo");
- countries.put("ER", "Africa/Asmara");
- countries.put("ES", "Europe/Madrid");
- countries.put("EST", "America/New_York");
- countries.put("FI", "Europe/Helsinki");
- countries.put("FR", "Europe/Paris");
- countries.put("GB", "Europe/London");
- countries.put("GMT", "GMT");
- countries.put("GR", "Europe/Athens");
- countries.put("HK", "Asia/Hong_Kong");
- countries.put("HST", "Pacific/Honolulu");
- countries.put("IE", "Europe/Dublin");
- countries.put("IL", "Asia/Tel_Aviv");
- countries.put("IN", "Asia/Kolkata");
- countries.put("IQ", "Asia/Baghdad");
- countries.put("IR", "Asia/Tehran");
- countries.put("IS", "Atlantic/Reykjavik");
- countries.put("IT", "Europe/Rome");
- countries.put("JM", "Jamaica");
- countries.put("JP", "Asia/Tokyo");
- countries.put("LY", "Africa/Tripoli");
- countries.put("MA", "Africa/Casablanca");
- countries.put("MDT", "America/Denver");
- countries.put("MH", "Kwajalein");
- countries.put("MQ", "America/Martinique");
- countries.put("MST", "America/Denver");
- countries.put("MX", "America/Mexico_City");
- countries.put("NL", "Europe/Amsterdam");
- countries.put("NO", "Europe/Oslo");
- countries.put("NP", "Asia/Katmandu");
- countries.put("NZ", "Pacific/Auckland");
- countries.put("PDT", "America/Los_Angeles");
- countries.put("PH", "Asia/Manila");
- countries.put("PK", "Asia/Karachi");
- countries.put("PL", "Europe/Warsaw");
- countries.put("PST", "America/Los_Angeles");
- countries.put("PT", "Europe/Lisbon");
- countries.put("PR", "America/Puerto_Rico");
- countries.put("RU", "Europe/Moscow");
- countries.put("SE", "Europe/Stockholm");
- countries.put("SG", "Asia/Singapore");
- countries.put("TH", "Asia/Bangkok");
- countries.put("TM", "Asia/Ashgabat");
- countries.put("TN", "Africa/Tunis");
- countries.put("TR", "Europe/Istanbul");
- countries.put("TW", "Asia/Taipei");
- countries.put("UK", "Europe/London");
- countries.put("US", "America/New_York");
- countries.put("UTC", "UTC");
- countries.put("VA", "Europe/Vatican");
- countries.put("VE", "America/Caracas");
- countries.put("VN", "Asia/Ho_Chi_Minh");
- countries.put("ZA", "Africa/Johannesburg");
- countries.put("ZULU", "Zulu");
- countries.put("INTERNET", BEATS_KEYWORD);
- countries.put("BEATS", BEATS_KEYWORD);
-
- ZoneId.getAvailableZoneIds().stream()
- .filter(tz -> !tz.contains("/") && tz.length() == 3 && !countries.containsKey(tz))
- .forEach(tz -> countries.put(tz, tz));
-
- COUNTRIES_MAP = Collections.unmodifiableMap(countries);
- }
-
- /**
- * Creates a new {@link WorldTime} instance.
- */
- public WorldTime() {
- super();
- commands.add(TIME_CMD);
- }
-
- /**
- * Returns the current Internet (beat) Time.
- *
- * @return The Internet Time string.
- */
- private static String internetTime() {
- final ZonedDateTime zdt = ZonedDateTime.now(ZoneId.of("UTC+01:00"));
- final int beats = (int) ((zdt.get(ChronoField.SECOND_OF_MINUTE) + (zdt.get(ChronoField.MINUTE_OF_HOUR) * 60)
- + (zdt.get(ChronoField.HOUR_OF_DAY) * 3600)) / 86.4);
- return String.format("%c%03d", '@', beats);
- }
-
- /**
- * Returns the world time.
- *
- *
- *
PST
- *
BEATS
- *
- *
- * @param query The query.
- * @return The {@link Message} containing the world time.
- */
- @SuppressFBWarnings("STT_STRING_PARSING_A_FIELD")
- static Message worldTime(final String query) {
- final String tz = (COUNTRIES_MAP.get((query.substring(query.indexOf(' ') + 1).trim()
- .toUpperCase(Constants.LOCALE))));
- final String response;
-
- if (tz != null) {
- if (BEATS_KEYWORD.equals(tz)) {
- response = ("The current Internet Time is: " + internetTime() + ' ' + BEATS_KEYWORD);
- } else {
- response = ZonedDateTime.now().withZoneSameInstant(ZoneId.of(tz)).format(
- DateTimeFormatter.ofPattern("'The time is 'HH:mm' on 'EEEE, d MMMM yyyy' in '"))
- + tz.substring(tz.indexOf('/') + 1).replace('_', ' ');
- }
- } else {
- return new ErrorMessage("The supported countries/zones are: " + COUNTRIES_MAP.keySet());
- }
-
- return new PublicMessage(response);
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void commandResponse(final Mobibot bot,
- final String sender,
- final String cmd,
- final String args,
- final boolean isPrivate) {
- final Message msg = worldTime(args);
-
- if (isPrivate) {
- bot.send(sender, msg.getMessage(), true);
- } else {
- if (msg.isError()) {
- bot.send(sender, msg.getMessage());
- } else {
- bot.send(msg.getMessage());
- }
- }
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void helpResponse(final Mobibot bot, final String sender, final String args, final boolean isPrivate) {
- bot.send(sender, "To display a country's current date/time:");
- bot.send(sender, bot.helpIndent(bot.getNick() + ": " + TIME_CMD) + " []");
-
- bot.send(sender, "For a listing of the supported countries:");
- bot.send(sender, bot.helpIndent(bot.getNick() + ": " + TIME_CMD));
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public boolean isPrivateMsgEnabled() {
- return true;
- }
-}
diff --git a/src/main/java/net/thauvin/erik/mobibot/msg/Message.java b/src/main/java/net/thauvin/erik/mobibot/msg/Message.java
deleted file mode 100644
index cc70c2a..0000000
--- a/src/main/java/net/thauvin/erik/mobibot/msg/Message.java
+++ /dev/null
@@ -1,193 +0,0 @@
-/*
- * Message.java
- *
- * Copyright (c) 2004-2020, 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.msg;
-
-import org.jibble.pircbot.Colors;
-
-/**
- * The Message class.
- *
- * @author Erik C. Thauvin
- * @created 2019-04-07
- * @since 1.0
- */
-public class Message {
- private String color = Colors.NORMAL;
- private boolean isError;
- private boolean isNotice;
- private boolean isPrivate;
- private String msg = "";
-
- /**
- * Creates a new message.
- */
- public Message() {
- // This constructor is intentionally empty
- }
-
- /**
- * Creates a new message.
- *
- * @param message The message.
- * @param isNotice The notice flag.
- * @param isError The error flag.
- * @param isPrivate The Private message
- */
- @SuppressWarnings("unused")
- public Message(final String message, final boolean isNotice, final boolean isError, final boolean isPrivate) {
- msg = message;
- this.isNotice = isNotice;
- this.isError = isError;
- this.isPrivate = isPrivate;
- }
-
-
- /**
- * Creates a new message.
- *
- * @param message The message.
- * @param isNotice The notice flag.
- * @param isError The error flag.
- * @param isPrivate The Private message
- * @param color The color.
- */
- @SuppressWarnings("unused")
- public Message(final String message,
- final boolean isNotice,
- final boolean isError,
- final boolean isPrivate,
- final String color) {
- msg = message;
- this.isNotice = isNotice;
- this.isError = isError;
- this.isPrivate = isPrivate;
- this.color = color;
- }
-
- /**
- * Returns the message color.
- *
- * @return The color.
- */
- public String getColor() {
- return color;
- }
-
- /**
- * Returns the message.
- *
- * @return The message.
- */
- public String getMessage() {
- return msg;
- }
-
- /**
- * Returns the message error flag.
- *
- * @return The error flag.
- */
- public boolean isError() {
- return isError;
- }
-
- /**
- * Returns the message notice flag.
- *
- * @return The notice flag.
- */
- public boolean isNotice() {
- return isNotice;
- }
-
- /**
- * Returns the message private flag.
- *
- * @return The private flag.
- */
- public boolean isPrivate() {
- return isPrivate;
- }
-
- /**
- * Set the color.
- *
- * @param color The new color.
- */
- public void setColor(final String color) {
- this.color = color;
- }
-
- /**
- * Sets the message error flag.
- *
- * @param error The error flag.
- */
- public void setError(final boolean error) {
- isError = error;
- }
-
- /**
- * Sets the message.
- *
- * @param message The new message.
- */
- public void setMessage(final String message) {
- msg = message;
- }
-
- /**
- * Sets the message notice flag.
- *
- * @param isNotice The notice flag.
- */
- public void setNotice(final boolean isNotice) {
- this.isNotice = isNotice;
- }
-
- /**
- * Sets the message private flag.
- *
- * @param isPrivate The private flag.
- */
- @SuppressWarnings("unused")
- public void setPrivate(final boolean isPrivate) {
- this.isPrivate = isPrivate;
- }
-
- @Override
- public String toString() {
- return "Message{" + "color='" + color + '\'' + ", isError=" + isError + ", isNotice=" + isNotice
- + ", isPrivate=" + isPrivate + ", msg='" + msg + '\'' + '}';
- }
-}
diff --git a/src/main/java/net/thauvin/erik/mobibot/tell/Tell.java b/src/main/java/net/thauvin/erik/mobibot/tell/Tell.java
deleted file mode 100644
index 9692505..0000000
--- a/src/main/java/net/thauvin/erik/mobibot/tell/Tell.java
+++ /dev/null
@@ -1,349 +0,0 @@
-/*
- * Tell.java
- *
- * Copyright (c) 2004-2020, 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.tell;
-
-import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
-import net.thauvin.erik.mobibot.Commands;
-import net.thauvin.erik.mobibot.Mobibot;
-import net.thauvin.erik.mobibot.Utils;
-import org.apache.commons.lang3.StringUtils;
-
-import java.util.List;
-import java.util.concurrent.CopyOnWriteArrayList;
-
-/**
- * The Tell command.
- *
- * @author Erik C. Thauvin
- * @created 2016-07-02
- * @since 1.0
- */
-public class Tell {
- /**
- * The tell command.
- */
- public static final String TELL_CMD = "tell";
-
- // Default maximum number of days to keep messages
- private static final int DEFAULT_TELL_MAX_DAYS = 7;
- // Default message max queue size
- private static final int DEFAULT_TELL_MAX_SIZE = 50;
- // Serialized object file extension
- private static final String SER_EXT = ".ser";
- // All keyword
- private static final String TELL_ALL_KEYWORD = "all";
- //T he delete command.
- private static final String TELL_DEL_KEYWORD = "del";
-
- // Bot instance
- private final Mobibot bot;
- // Maximum number of days to keep messages
- private final int maxDays;
- // Message maximum queue size
- private final int maxSize;
- // Messages queue
- private final List messages = new CopyOnWriteArrayList<>();
- // Serialized object file
- private final String serializedObject;
-
- /**
- * Creates a new instance.
- *
- * @param bot The bot.
- * @param maxDays Max days.
- * @param maxSize Max size.
- */
- public Tell(final Mobibot bot, final String maxDays, final String maxSize) {
- this.bot = bot;
- this.maxDays = Utils.getIntProperty(maxDays, DEFAULT_TELL_MAX_DAYS);
- this.maxSize = Utils.getIntProperty(maxSize, DEFAULT_TELL_MAX_SIZE);
-
- // Load the message queue
- serializedObject = bot.getLogsDir() + bot.getName() + SER_EXT;
- messages.addAll(TellMessagesMgr.load(serializedObject, bot.getLogger()));
-
- if (clean()) {
- save();
- }
- }
-
- /**
- * Cleans the messages queue.
- *
- * @return true if the queue was cleaned.
- */
- @SuppressWarnings("WeakerAccess")
- final boolean clean() {
- if (bot.getLogger().isDebugEnabled()) {
- bot.getLogger().debug("Cleaning the messages.");
- }
-
- return TellMessagesMgr.clean(messages, maxDays);
- }
-
- /**
- * Responds with help.
- *
- * @param sender The sender.
- */
- public void helpResponse(final String sender) {
- bot.send(sender, "To send a message to someone when they join the channel:");
- bot.send(sender, bot.helpIndent(bot.getNick() + ": " + TELL_CMD + " "));
-
- bot.send(sender, "To view queued and sent messages:");
- bot.send(sender, bot.helpIndent(bot.getNick() + ": " + TELL_CMD + ' ' + Commands.VIEW_CMD));
-
- bot.send(sender, "Messages are kept for " + Utils.bold(maxDays) + Utils.plural(maxDays, " day.", " days."));
- }
-
- /**
- * Returns true if enabled.
- *
- * @return true or false
- */
- public boolean isEnabled() {
- return maxSize > 0 && maxDays > 0;
- }
-
- /**
- * Processes the commands.
- *
- * @param sender The sender's nick.
- * @param cmds The commands string.
- */
- @SuppressFBWarnings(value = "CC_CYCLOMATIC_COMPLEXITY", justification = "Working on it.")
- public void response(final String sender, final String cmds) {
- final String arrow = " --> ";
- if (StringUtils.isBlank(cmds)) {
- helpResponse(sender);
- } else if (cmds.startsWith(Commands.VIEW_CMD)) {
- if (bot.isOp(sender) && (Commands.VIEW_CMD + ' ' + TELL_ALL_KEYWORD).equals(cmds)) {
- if (!messages.isEmpty()) {
- for (final TellMessage message : messages) {
- bot.send(sender, Utils.bold(message.getSender()) + arrow + Utils.bold(message.getRecipient())
- + " [ID: " + message.getId() + ", "
- + (message.isReceived() ? "DELIVERED" : "QUEUED") + ']',
- true);
- }
- } else {
- bot.send(sender, "There are no messages in the queue.", true);
- }
- } else {
- boolean hasMessage = false;
-
- for (final TellMessage message : messages) {
- if (message.isMatch(sender)) {
- if (!hasMessage) {
- hasMessage = true;
- bot.send(sender, "Here are your messages: ", true);
- }
-
- if (message.isReceived()) {
- bot.send(sender,
- Utils.bold(message.getSender()) + arrow + Utils.bold(message.getRecipient())
- + " [" + Utils.utcDateTime(message.getReceived()) + ", ID: "
- + message.getId() + ", DELIVERED]",
- true);
-
- } else {
- bot.send(sender,
- Utils.bold(message.getSender()) + arrow + Utils.bold(message.getRecipient())
- + " [" + Utils.utcDateTime(message.getQueued()) + ", ID: "
- + message.getId() + ", QUEUED]",
- true);
- }
-
- bot.send(sender, bot.helpIndent(message.getMessage(), false), true);
- }
- }
-
- if (!hasMessage) {
- bot.send(sender, "You have no messages in the queue.", true);
- } else {
- bot.send(sender, "To delete one or all delivered messages:");
- bot.send(sender,
- bot.helpIndent(bot.getNick() + ": " + TELL_CMD + ' ' + TELL_DEL_KEYWORD + " '));
- bot.send(sender, "Messages are kept for " + Utils.bold(maxDays)
- + Utils.plural(maxDays, " day.", " days."));
- }
- }
- } else if (cmds.startsWith(TELL_DEL_KEYWORD + ' ')) {
- final String[] split = cmds.split(" ");
-
- if (split.length == 2) {
- final String id = split[1];
- boolean deleted = false;
-
- if (TELL_ALL_KEYWORD.equalsIgnoreCase(id)) {
- for (final TellMessage message : messages) {
- if (message.getSender().equalsIgnoreCase(sender) && message.isReceived()) {
- messages.remove(message);
- deleted = true;
- }
- }
-
- if (deleted) {
- save();
- bot.send(sender, "Delivered messages have been deleted.", true);
- } else {
- bot.send(sender, "No delivered messages were found.", true);
- }
-
- } else {
- boolean found = false;
-
- for (final TellMessage message : messages) {
- found = message.isMatchId(id);
-
- if (found && (message.getSender().equalsIgnoreCase(sender) || bot.isOp(sender))) {
- messages.remove(message);
-
- save();
- bot.send(sender, "Your message was deleted from the queue.", true);
- deleted = true;
- break;
- }
- }
-
- if (!deleted) {
- if (found) {
- bot.send(sender, "Only messages that you sent can be deleted.", true);
- } else {
- bot.send(sender, "The specified message [ID " + id + "] could not be found.", true);
- }
- }
- }
- } else {
- helpResponse(sender);
- }
- } else {
- final String[] split = cmds.split(" ", 2);
-
- if (split.length == 2 && (StringUtils.isNotBlank(split[1]) && split[1].contains(" "))) {
- if (messages.size() < maxSize) {
- final TellMessage message = new TellMessage(sender, split[0], split[1].trim());
-
- messages.add(message);
-
- save();
-
- bot.send(sender, "Message [ID " + message.getId() + "] was queued for "
- + Utils.bold(message.getRecipient()), true);
- } else {
- bot.send(sender, "Sorry, the messages queue is currently full.", true);
- }
- } else {
- helpResponse(sender);
- }
- }
-
- if (clean()) {
- save();
- }
- }
-
- /**
- * Saves the messages queue.
- */
- @SuppressWarnings("WeakerAccess")
- final void save() {
- TellMessagesMgr.save(serializedObject, messages, bot.getLogger());
- }
-
- /**
- * Checks and sends messages.
- *
- * @param nickname The user's nickname.
- * @param isMessage The message flag.
- */
- public void send(final String nickname, final boolean isMessage) {
- if (!nickname.equals(bot.getNick()) && isEnabled()) {
- messages.stream().filter(message -> message.isMatch(nickname)).forEach(
- message -> {
- if (message.getRecipient().equalsIgnoreCase(nickname) && !message.isReceived()) {
- if (message.getSender().equals(nickname)) {
- if (!isMessage) {
- bot.send(nickname, Utils.bold("You") + " wanted me to remind you: "
- + Utils.reverseColor(message.getMessage()),
- true);
-
- message.setIsReceived();
- message.setIsNotified();
-
- save();
- }
- } else {
- bot.send(nickname, message.getSender() + " wanted me to tell you: "
- + Utils.reverseColor(message.getMessage()),
- true);
-
- message.setIsReceived();
-
- save();
- }
- } else if (message.getSender().equalsIgnoreCase(nickname) && message.isReceived()
- && !message.isNotified()) {
- bot.send(nickname,
- "Your message "
- + Utils.reverseColor("[ID " + message.getId() + ']') + " was sent to "
- + Utils.bold(message.getRecipient()) + " on "
- + Utils.utcDateTime(message.getReceived()),
- true);
-
- message.setIsNotified();
-
- save();
- }
- });
- }
- }
-
- /**
- * Checks and sends messages.
- *
- * @param nickname The user's nickname.
- */
- public void send(final String nickname) {
- send(nickname, false);
- }
-
- /**
- * Returns the messages queue size.
- *
- * @return The size.
- */
- public int size() {
- return messages.size();
- }
-}
diff --git a/src/main/java/net/thauvin/erik/mobibot/tell/TellMessage.java b/src/main/java/net/thauvin/erik/mobibot/tell/TellMessage.java
deleted file mode 100644
index 9d39e22..0000000
--- a/src/main/java/net/thauvin/erik/mobibot/tell/TellMessage.java
+++ /dev/null
@@ -1,181 +0,0 @@
-/*
- * TellMessage.java
- *
- * Copyright (c) 2004-2019, 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.tell;
-
-import java.io.Serializable;
-import java.time.Clock;
-import java.time.LocalDateTime;
-import java.time.format.DateTimeFormatter;
-
-/**
- * The TellMessage class.
- *
- * @author Erik C. Thauvin
- * @created 2014-04-24
- * @since 1.0
- */
-@SuppressWarnings("PMD.DataClass")
-public class TellMessage implements Serializable {
- private static final long serialVersionUID = 2L;
- private final String id;
- private final String message;
- private final LocalDateTime queued;
- private final String recipient;
- private final String sender;
- private boolean isNotified;
- private boolean isReceived;
- private LocalDateTime received;
-
- /**
- * Create a new message.
- *
- * @param sender The sender's nick.
- * @param recipient The recipient's nick.
- * @param message The message.
- */
- TellMessage(final String sender, final String recipient, final String message) {
- this.sender = sender;
- this.recipient = recipient;
- this.message = message;
-
- this.queued = LocalDateTime.now(Clock.systemUTC());
- this.id = this.queued.format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
- }
-
- /**
- * Returns the message id.
- *
- * @return The message id.
- */
- public String getId() {
- return id;
- }
-
- /**
- * Returns the message text.
- *
- * @return The text of the message.
- */
- public String getMessage() {
- return message;
- }
-
- /**
- * Returns the queued date/time.
- *
- * @return true if the message is queued.
- */
- LocalDateTime getQueued() {
- return queued;
- }
-
- /**
- * Returns the state of the received flag.
- *
- * @return true if the message has been received.
- */
- public LocalDateTime getReceived() {
- return received;
- }
-
- /**
- * Returns the message's recipient.
- *
- * @return The recipient of the message.
- */
- String getRecipient() {
- return recipient;
- }
-
- /**
- * Returns the message's sender.
- *
- * @return The sender of the message.
- */
- public String getSender() {
- return sender;
- }
-
- /**
- * Matches the message sender or recipient.
- *
- * @param nick The nickname to match with.
- * @return true if the nickname matches.
- */
- boolean isMatch(final String nick) {
- return (sender.equalsIgnoreCase(nick) || recipient.equalsIgnoreCase(nick));
- }
-
- /**
- * Match the message ID.
- *
- * @param id The ID to match with.
- * @return true if the id matches.
- */
- boolean isMatchId(final String id) {
- return this.id.equals(id);
- }
-
- /**
- * Returns the notification flag state.
- *
- * @return true if the sender has been notified.
- */
- boolean isNotified() {
- return isNotified;
- }
-
- /**
- * Returns the received flag state.
- *
- * @return true if the message was received.
- */
- public boolean isReceived() {
- return isReceived;
- }
-
- /**
- * Sets the notified flag.
- */
- void setIsNotified() {
- isNotified = true;
- }
-
- /**
- * Sets the received flag.
- */
- void setIsReceived() {
- received = LocalDateTime.now(Clock.systemUTC());
- isReceived = true;
- }
-}
diff --git a/src/main/java/net/thauvin/erik/mobibot/tell/TellMessagesMgr.java b/src/main/java/net/thauvin/erik/mobibot/tell/TellMessagesMgr.java
deleted file mode 100644
index ad6cb0a..0000000
--- a/src/main/java/net/thauvin/erik/mobibot/tell/TellMessagesMgr.java
+++ /dev/null
@@ -1,134 +0,0 @@
-/*
- * TellMessagesMgr.java
- *
- * Copyright (c) 2004-2020, 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.tell;
-
-import org.apache.logging.log4j.Logger;
-
-import java.io.BufferedInputStream;
-import java.io.BufferedOutputStream;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.io.ObjectInput;
-import java.io.ObjectInputStream;
-import java.io.ObjectOutput;
-import java.io.ObjectOutputStream;
-import java.nio.file.Files;
-import java.nio.file.Paths;
-import java.time.Clock;
-import java.time.LocalDateTime;
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * The Tell Messages Manager.
- *
- * @author Erik C. Thauvin
- * @created 2014-04-26
- * @since 1.0
- */
-final class TellMessagesMgr {
- /**
- * Disables the default constructor.
- *
- * @throws UnsupportedOperationException If the constructor is called.
- */
- private TellMessagesMgr() {
- throw new UnsupportedOperationException("Illegal constructor call.");
- }
-
- /**
- * Cleans the messages queue.
- *
- * @param tellMessages The messages list.
- * @param tellMaxDays The maximum number of days to keep messages for.
- * @return True if the queue was cleaned.
- */
- static boolean clean(final List tellMessages, final int tellMaxDays) {
- final LocalDateTime today = LocalDateTime.now(Clock.systemUTC());
-
- return tellMessages.removeIf(o -> o.getQueued().plusDays(tellMaxDays).isBefore(today));
- }
-
- /**
- * Loads the messages.
- *
- * @param file The serialized objects file.
- * @param logger The logger.
- * @return The {@link TellMessage} array.
- */
- @SuppressWarnings("unchecked")
- public static List load(final String file, final Logger logger) {
- try {
-
- try (final ObjectInput input = new ObjectInputStream(
- new BufferedInputStream(Files.newInputStream(Paths.get(file))))) {
- if (logger.isDebugEnabled()) {
- logger.debug("Loading the messages.");
- }
-
- return ((List) input.readObject());
- }
- } catch (FileNotFoundException ignore) {
- // Do nothing
- } catch (IOException e) {
- logger.error("An IO error occurred loading the messages queue.", e);
- } catch (Exception e) {
- logger.error("An error occurred loading the messages queue.", e);
- }
-
- return new ArrayList<>();
- }
-
- /**
- * Saves the messages.
- *
- * @param file The serialized objects file.
- * @param messages The {@link TellMessage} array.
- * @param logger The logger.
- */
- public static void save(final String file, final List messages, final Logger logger) {
- try {
-
- try (final ObjectOutput output = new ObjectOutputStream(
- new BufferedOutputStream(Files.newOutputStream(Paths.get(file))))) {
- if (logger.isDebugEnabled()) {
- logger.debug("Saving the messages.");
- }
-
- output.writeObject(messages);
- }
- } catch (IOException e) {
- logger.error("Unable to save messages queue.", e);
- }
- }
-}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/Addons.kt b/src/main/kotlin/net/thauvin/erik/mobibot/Addons.kt
new file mode 100644
index 0000000..8ae52a9
--- /dev/null
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/Addons.kt
@@ -0,0 +1,164 @@
+/*
+ * Addons.kt
+ *
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package net.thauvin.erik.mobibot
+
+import net.thauvin.erik.mobibot.Utils.notContains
+import net.thauvin.erik.mobibot.commands.AbstractCommand
+import net.thauvin.erik.mobibot.commands.links.LinksMgr
+import net.thauvin.erik.mobibot.modules.AbstractModule
+import org.pircbotx.hooks.events.PrivateMessageEvent
+import org.pircbotx.hooks.types.GenericMessageEvent
+import java.util.Properties
+
+/**
+ * Modules and Commands addons.
+ */
+class Addons(private val props: Properties) {
+ private val disabledModules = props.getProperty("disabled-modules", "").split(LinksMgr.TAG_MATCH.toRegex())
+ private val disableCommands = props.getProperty("disabled-commands", "").split(LinksMgr.TAG_MATCH.toRegex())
+
+ val commands: MutableList = mutableListOf()
+ val modules: MutableList = mutableListOf()
+ val names = Names
+
+ /**
+ * Add a module with properties.
+ */
+ fun add(module: AbstractModule) {
+ with(module) {
+ if (disabledModules.notContains(name, true)) {
+ if (hasProperties()) {
+ propertyKeys.forEach {
+ setProperty(it, props.getProperty(it, ""))
+ }
+ }
+
+ if (isEnabled) {
+ modules.add(this)
+ names.modules.add(name)
+ names.commands.addAll(commands)
+ }
+ }
+ }
+ }
+
+ /**
+ * Add a command with properties.
+ */
+ fun add(command: AbstractCommand) {
+ with(command) {
+ if (disableCommands.notContains(name, true)) {
+ if (properties.isNotEmpty()) {
+ properties.keys.forEach {
+ setProperty(it, props.getProperty(it, ""))
+ }
+ }
+ if (isEnabled()) {
+ commands.add(this)
+ if (isVisible) {
+ if (isOpOnly) {
+ names.ops.add(name)
+ } else {
+ names.commands.add(name)
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Execute a command or module.
+ */
+ fun exec(channel: String, cmd: String, args: String, event: GenericMessageEvent): Boolean {
+ val cmds = if (event is PrivateMessageEvent) commands else commands.filter { it.isPublic }
+ for (command in cmds) {
+ if (command.name.startsWith(cmd)) {
+ command.commandResponse(channel, args, event)
+ return true
+ }
+ }
+ val mods = if (event is PrivateMessageEvent) modules.filter { it.isPrivateMsgEnabled } else modules
+ for (module in mods) {
+ if (module.commands.contains(cmd)) {
+ module.commandResponse(channel, cmd, args, event)
+ return true
+ }
+ }
+ return false
+ }
+
+ /**
+ * Match a command.
+ */
+ fun match(channel: String, event: GenericMessageEvent): Boolean {
+ for (command in commands) {
+ if (command.matches(event.message)) {
+ command.commandResponse(channel, event.message, event)
+ return true
+ }
+ }
+ return false
+ }
+
+ /**
+ * Commands and Modules help.
+ */
+ fun help(channel: String, topic: String, event: GenericMessageEvent): Boolean {
+ for (command in commands) {
+ if (command.isVisible && command.name.startsWith(topic)) {
+ return command.helpResponse(channel, topic, event)
+ }
+ }
+ for (module in modules) {
+ if (module.commands.contains(topic)) {
+ return module.helpResponse(event)
+ }
+ }
+ return false
+ }
+
+ /**
+ * Holds commands and modules names.
+ */
+ object Names {
+ val modules: MutableList = mutableListOf()
+ val commands: MutableList = mutableListOf()
+ val ops: MutableList = mutableListOf()
+
+ fun sort() {
+ modules.sort()
+ commands.sort()
+ ops.sort()
+ }
+ }
+}
diff --git a/src/main/java/net/thauvin/erik/mobibot/Constants.java b/src/main/kotlin/net/thauvin/erik/mobibot/Constants.kt
similarity index 64%
rename from src/main/java/net/thauvin/erik/mobibot/Constants.java
rename to src/main/kotlin/net/thauvin/erik/mobibot/Constants.kt
index dd2cbaa..f96e30b 100644
--- a/src/main/java/net/thauvin/erik/mobibot/Constants.java
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/Constants.kt
@@ -1,7 +1,7 @@
/*
- * Constants.java
+ * Constants.kt
*
- * Copyright (c) 2004-2019, Erik C. Thauvin (erik@thauvin.net)
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -29,48 +29,69 @@
* 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 java.util.Locale;
+package net.thauvin.erik.mobibot
/**
- * The Constants class.
- *
- * @author Erik C. Thauvin
- * @created 2019-04-19
- * @since 1.0
+ * The `Constants`.
*/
-public final class Constants {
+object Constants {
/**
* The connect/read timeout in ms.
*/
- public static final int CONNECT_TIMEOUT = 5000;
+ const val CONNECT_TIMEOUT = 5000
+
/**
- * Default locale.
+ * Debug command line argument.
*/
- public static final Locale LOCALE = Locale.getDefault();
+ const val DEBUG_ARG = "debug"
+
+ /**
+ * Default IRC Port.
+ */
+ const val DEFAULT_PORT = 6667
+
+ /**
+ * Default IRC Server.
+ */
+ const val DEFAULT_SERVER = "irc.libera.chat"
+
+ /**
+ * Help command line argument.
+ */
+ const val HELP_ARG = "help"
+
+ /**
+ * The help command.
+ */
+ const val HELP_CMD = "help"
+
+ /**
+ * The link command.
+ */
+ const val LINK_CMD = "L"
+
/**
* The empty title string.
*/
- public static final String NO_TITLE = "No Title";
+ const val NO_TITLE = "No Title"
+
+ /**
+ * Properties command line argument.
+ */
+ const val PROPS_ARG = "properties"
+
+ /**
+ * The tag command
+ */
+ const val TAG_CMD = "T"
+
/**
* The timer delay in minutes.
*/
- public static final long TIMER_DELAY = 10L;
- /**
- * The twitter post flag property key.
- */
- public static final String TWITTER_AUTOPOST_PROP = "twitter-auto-post";
- /**
- * The Twitter handle property key.
- */
- public static final String TWITTER_HANDLE_PROP = "twitter-handle";
+ const val TIMER_DELAY = 10L
/**
- * Disables the default constructor.
+ * Properties version line argument.
*/
- private Constants() {
- throw new UnsupportedOperationException("Illegal constructor call.");
- }
+ const val VERSION_ARG = "version"
}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/FeedReader.kt b/src/main/kotlin/net/thauvin/erik/mobibot/FeedReader.kt
new file mode 100644
index 0000000..8fc4f2a
--- /dev/null
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/FeedReader.kt
@@ -0,0 +1,93 @@
+/*
+ * FeedReader.kt
+ *
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package net.thauvin.erik.mobibot
+
+import com.rometools.rome.io.FeedException
+import com.rometools.rome.io.SyndFeedInput
+import com.rometools.rome.io.XmlReader
+import net.thauvin.erik.mobibot.Utils.green
+import net.thauvin.erik.mobibot.Utils.helpFormat
+import net.thauvin.erik.mobibot.Utils.sendMessage
+import net.thauvin.erik.mobibot.entries.FeedsMgr
+import net.thauvin.erik.mobibot.msg.Message
+import net.thauvin.erik.mobibot.msg.NoticeMessage
+import org.pircbotx.hooks.types.GenericMessageEvent
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+import java.io.IOException
+import java.net.URL
+
+/**
+ * Reads an RSS feed.
+ */
+class FeedReader(private val url: String, val event: GenericMessageEvent) : Runnable {
+ private val logger: Logger = LoggerFactory.getLogger(FeedsMgr::class.java)
+
+ /**
+ * Fetches the Feed's items.
+ */
+ override fun run() {
+ try {
+ readFeed(url).forEach {
+ event.sendMessage("", it)
+ }
+ } catch (e: FeedException) {
+ if (logger.isWarnEnabled) logger.warn("Unable to parse the feed at $url", e)
+ event.sendMessage("An error has occurred while parsing the feed: ${e.message}")
+ } catch (e: IOException) {
+ if (logger.isWarnEnabled) logger.warn("Unable to fetch the feed at $url", e)
+ event.sendMessage("An error has occurred while fetching the feed: ${e.message}")
+ }
+ }
+
+ companion object {
+ @JvmStatic
+ @Throws(FeedException::class, IOException::class)
+ fun readFeed(url: String, maxItems: Int = 5): List {
+ val messages = mutableListOf()
+ val input = SyndFeedInput()
+ XmlReader(URL(url)).use { reader ->
+ val feed = input.build(reader)
+ val items = feed.entries
+ if (items.isEmpty()) {
+ messages.add(NoticeMessage("There is currently nothing to view."))
+ } else {
+ items.take(maxItems).forEach {
+ messages.add(NoticeMessage(it.title))
+ messages.add(NoticeMessage(helpFormat(it.link.green(), false)))
+ }
+ }
+ }
+ return messages
+ }
+ }
+}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/Mobibot.kt b/src/main/kotlin/net/thauvin/erik/mobibot/Mobibot.kt
new file mode 100644
index 0000000..d8ff578
--- /dev/null
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/Mobibot.kt
@@ -0,0 +1,441 @@
+/*
+ * Mobibot.kt
+ *
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package net.thauvin.erik.mobibot
+
+import net.thauvin.erik.mobibot.Utils.appendIfMissing
+import net.thauvin.erik.mobibot.Utils.bot
+import net.thauvin.erik.mobibot.Utils.getIntProperty
+import net.thauvin.erik.mobibot.Utils.isChannelOp
+import net.thauvin.erik.mobibot.Utils.lastOrEmpty
+import net.thauvin.erik.mobibot.Utils.sendList
+import net.thauvin.erik.mobibot.Utils.sendMessage
+import net.thauvin.erik.mobibot.Utils.toIsoLocalDate
+import net.thauvin.erik.mobibot.commands.ChannelFeed
+import net.thauvin.erik.mobibot.commands.Cycle
+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.Me
+import net.thauvin.erik.mobibot.commands.Modules
+import net.thauvin.erik.mobibot.commands.Msg
+import net.thauvin.erik.mobibot.commands.Nick
+import net.thauvin.erik.mobibot.commands.Recap
+import net.thauvin.erik.mobibot.commands.Recap.Companion.storeRecap
+import net.thauvin.erik.mobibot.commands.Say
+import net.thauvin.erik.mobibot.commands.Users
+import net.thauvin.erik.mobibot.commands.Versions
+import net.thauvin.erik.mobibot.commands.links.Comment
+import net.thauvin.erik.mobibot.commands.links.LinksMgr
+import net.thauvin.erik.mobibot.commands.links.Posting
+import net.thauvin.erik.mobibot.commands.links.Tags
+import net.thauvin.erik.mobibot.commands.links.View
+import net.thauvin.erik.mobibot.commands.tell.Tell
+import net.thauvin.erik.mobibot.modules.Calc
+import net.thauvin.erik.mobibot.modules.CryptoPrices
+import net.thauvin.erik.mobibot.modules.CurrencyConverter
+import net.thauvin.erik.mobibot.modules.Dice
+import net.thauvin.erik.mobibot.modules.GoogleSearch
+import net.thauvin.erik.mobibot.modules.Joke
+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.War
+import net.thauvin.erik.mobibot.modules.Weather2
+import net.thauvin.erik.mobibot.modules.WorldTime
+import net.thauvin.erik.semver.Version
+import org.apache.commons.cli.CommandLine
+import org.apache.commons.cli.CommandLineParser
+import org.apache.commons.cli.DefaultParser
+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.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
+import java.io.FileOutputStream
+import java.io.IOException
+import java.io.PrintStream
+import java.nio.file.Files
+import java.nio.file.Paths
+import java.util.Properties
+import java.util.regex.Pattern
+import kotlin.system.exitProcess
+
+@Version(properties = "version.properties", className = "ReleaseInfo", template = "version.mustache", type = "kt")
+class Mobibot(nickname: String, val channel: String, logsDirPath: String, p: Properties) : ListenerAdapter() {
+ // The bot configuration.
+ private val config: Configuration
+
+ // Commands and Modules
+ private val addons: Addons
+
+ // Tell module
+ private val tell: Tell
+
+ /** Logger. */
+ val logger: Logger = LoggerFactory.getLogger(Mobibot::class.java)
+
+ /**
+ * Connects to the server and joins the channel.
+ */
+ fun connect() {
+ PircBotX(config).startBot()
+ }
+
+ /**
+ * Responds with the default help.
+ */
+ private fun helpDefault(event: GenericMessageEvent) {
+ event.sendMessage("Type a URL on $channel to post it.")
+ event.sendMessage("For more information on a specific command, type:")
+ event.sendMessage(
+ Utils.helpFormat(
+ Utils.buildCmdSyntax(
+ "%c ${Constants.HELP_CMD} ",
+ event.bot().nick,
+ event is PrivateMessageEvent
+ )
+ ),
+ )
+ event.sendMessage("The commands are:")
+ event.sendList(addons.names.commands, 8, isBold = true, isIndent = true)
+ if (isChannelOp(channel, event)) {
+ event.sendMessage("The op commands are:")
+ event.sendList(addons.names.ops, 8, isBold = true, isIndent = true)
+ }
+ }
+
+ /**
+ * Responds with the default, commands or modules help.
+ */
+ private fun helpResponse(event: GenericMessageEvent, topic: String) {
+ if (topic.isBlank() || !addons.help(channel, topic.lowercase().trim(), event)) {
+ helpDefault(event)
+ }
+ }
+
+ override fun onAction(event: ActionEvent?) {
+ event?.channel?.let {
+ if (channel == it.name) {
+ event.user?.let { user ->
+ storeRecap(user.nick, event.action, true)
+ }
+ }
+ }
+ }
+
+ override fun onDisconnect(event: DisconnectEvent?) {
+ event?.let {
+ with(event.getBot()) {
+ LinksMgr.twitter.notification("$nick disconnected from irc://$serverHostname")
+ }
+ }
+ LinksMgr.twitter.shutdown()
+ }
+
+ override fun onPrivateMessage(event: PrivateMessageEvent?) {
+ event?.user?.let { user ->
+ if (logger.isTraceEnabled) logger.trace("<<< ${user.nick}: ${event.message}")
+ val cmds = event.message.trim().split(" ".toRegex(), 2)
+ val cmd = cmds[0].lowercase()
+ val args = cmds.lastOrEmpty().trim()
+ if (cmd.startsWith(Constants.HELP_CMD)) { // help
+ helpResponse(event, args)
+ } else if (!addons.exec(channel, cmd, args, event)) { // Execute command or module
+ helpDefault(event)
+ }
+ }
+ }
+
+ override fun onJoin(event: JoinEvent?) {
+ event?.user?.let { user ->
+ with(event.getBot()) {
+ if (user.nick == nick) {
+ LinksMgr.twitter.notification("$nick has joined ${event.channel.name} on irc://$serverHostname")
+ } else {
+ tell.send(event)
+ }
+ }
+ }
+ }
+
+ override fun onMessage(event: MessageEvent?) {
+ event?.user?.let { user ->
+ val sender = user.nick
+ val message = event.message
+ tell.send(event)
+ if (message.matches("(?i)${Pattern.quote(event.bot().nick)}:.*".toRegex())) { // mobibot:
+ 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 = cmds.lastOrEmpty().trim()
+ if (cmd.startsWith(Constants.HELP_CMD)) { // mobibot: help
+ helpResponse(event, args)
+ } else {
+ // Execute module or command
+ addons.exec(channel, cmd, args, event)
+ }
+ } else if (addons.match(channel, event)) { // Links, e.g.: https://www.example.com/ or L1: , etc.
+ if (logger.isTraceEnabled) logger.trace(">>> $sender: $message")
+ }
+ storeRecap(sender, message, false)
+ }
+ }
+
+ override fun onNickChange(event: NickChangeEvent?) {
+ event?.let { tell.send(event) }
+ }
+
+ override fun onPart(event: PartEvent?) {
+ event?.user?.let { user ->
+ with(event.getBot()) {
+ if (user.nick == nick) {
+ LinksMgr.twitter.notification("$nick has left ${event.channel.name} on irc://$serverHostname")
+ }
+ }
+ }
+ }
+
+ companion object {
+ @JvmStatic
+ @Throws(Exception::class)
+ fun main(args: Array) {
+ // Set up the command line options
+ val options = Options()
+ .addOption(
+ Constants.HELP_ARG.substring(0, 1),
+ Constants.HELP_ARG,
+ false,
+ "print this help message"
+ )
+ .addOption(
+ Constants.DEBUG_ARG.substring(0, 1), Constants.DEBUG_ARG, false,
+ "print debug & logging data directly to the console"
+ )
+ .addOption(
+ Option.builder(Constants.PROPS_ARG.substring(0, 1)).hasArg()
+ .argName("file")
+ .desc("use alternate properties file")
+ .longOpt(Constants.PROPS_ARG).build()
+ )
+ .addOption(
+ Constants.VERSION_ARG.substring(0, 1),
+ Constants.VERSION_ARG,
+ false,
+ "print version info"
+ )
+
+ // Parse the command line
+ val parser: CommandLineParser = DefaultParser()
+ val commandLine: CommandLine
+ try {
+ commandLine = parser.parse(options, args)
+ } catch (e: ParseException) {
+ System.err.println("CLI Parsing failed. Reason: ${e.message}")
+ exitProcess(1)
+ }
+ when {
+ commandLine.hasOption(Constants.HELP_ARG[0]) -> {
+ // Output the usage
+ HelpFormatter().printHelp(Mobibot::class.java.name, options)
+ }
+ commandLine.hasOption(Constants.VERSION_ARG[0]) -> {
+ // Output the version
+ println("${ReleaseInfo.PROJECT} ${ReleaseInfo.VERSION} (${ReleaseInfo.BUILDDATE.toIsoLocalDate()})")
+ println(ReleaseInfo.WEBSITE)
+ }
+ else -> {
+ // Load the properties
+ val p = Properties()
+ try {
+ Files.newInputStream(
+ Paths.get(commandLine.getOptionValue(Constants.PROPS_ARG[0], "./mobibot.properties"))
+ ).use { fis ->
+ p.load(fis)
+ }
+ } catch (ignore: FileNotFoundException) {
+ System.err.println("Unable to find properties file.")
+ exitProcess(1)
+ } catch (ignore: IOException) {
+ System.err.println("Unable to open properties file.")
+ exitProcess(1)
+ }
+ val nickname = p.getProperty("nick", Mobibot::class.java.name.lowercase())
+ val channel = p.getProperty("channel")
+ val logsDir = p.getProperty("logs", ".").appendIfMissing(File.separatorChar)
+
+ // Redirect stdout and stderr
+ if (!commandLine.hasOption(Constants.DEBUG_ARG[0])) {
+ try {
+ val stdout = PrintStream(
+ BufferedOutputStream(
+ FileOutputStream(
+ logsDir + channel.substring(1) + '.' + Utils.today() + ".log", true
+ )
+ ), true
+ )
+ System.setOut(stdout)
+ } catch (ignore: IOException) {
+ System.err.println("Unable to open output (stdout) log file.")
+ exitProcess(1)
+ }
+ try {
+ val stderr = PrintStream(
+ BufferedOutputStream(
+ FileOutputStream("$logsDir$nickname.err", true)
+ ), true
+ )
+ System.setErr(stderr)
+ } catch (ignore: IOException) {
+ System.err.println("Unable to open error (stderr) log file.")
+ exitProcess(1)
+ }
+ }
+
+ // Start the bot
+ Mobibot(nickname, channel, logsDir, p).connect()
+ }
+ }
+ }
+ }
+
+ /**
+ * Initialize the bot.
+ */
+ init {
+ val ircServer = p.getProperty("server", Constants.DEFAULT_SERVER)
+ config = Configuration.Builder().apply {
+ name = nickname
+ login = p.getProperty("login", nickname)
+ realName = p.getProperty("realname", nickname)
+ addServer(
+ ircServer,
+ p.getIntProperty("port", Constants.DEFAULT_PORT)
+ )
+ addAutoJoinChannel(channel)
+ addListener(this@Mobibot)
+ version = "${ReleaseInfo.PROJECT} ${ReleaseInfo.VERSION}"
+ isAutoNickChange = true
+ val identPwd = p.getProperty("ident")
+ if (!identPwd.isNullOrBlank()) {
+ nickservPassword = identPwd
+ }
+ val identNick = p.getProperty("ident-nick")
+ if (!identNick.isNullOrBlank()) {
+ nickservNick = identNick
+ }
+ val identMsg = p.getProperty("ident-msg")
+ if (!identMsg.isNullOrBlank()) {
+ nickservCustomMessage = identMsg
+ }
+ isAutoReconnect = true
+ //socketConnectTimeout = Constants.CONNECT_TIMEOUT
+ //socketTimeout = Constants.CONNECT_TIMEOUT
+ //messageDelay = StaticDelay(500)
+ }.buildConfiguration()
+
+ // Load the current entries
+ with(LinksMgr) {
+ entries.channel = channel
+ entries.ircServer = ircServer
+ entries.logsDir = logsDirPath
+ entries.backlogs = p.getProperty("backlogs", "")
+ entries.load()
+
+ // Set up pinboard
+ pinboard.setApiToken(p.getProperty("pinboard-api-token", ""))
+ }
+
+ addons = Addons(p)
+
+ // Load the commands
+ addons.add(ChannelFeed(channel.removePrefix("#")))
+ addons.add(Comment())
+ addons.add(Cycle())
+ addons.add(Die())
+ addons.add(Ignore())
+ addons.add(LinksMgr())
+ addons.add(Me())
+ addons.add(Modules(addons.names.modules))
+ addons.add(Msg())
+ addons.add(Nick())
+ addons.add(Posting())
+ addons.add(Recap())
+ addons.add(Say())
+ addons.add(Tags())
+
+ // Tell command
+ tell = Tell("${logsDirPath}${nickname}.ser")
+ addons.add(tell)
+
+ addons.add(LinksMgr.twitter)
+ addons.add(Users())
+ addons.add(Versions())
+ addons.add(View())
+
+ // Load the modules
+ addons.add(Calc())
+ addons.add(CryptoPrices())
+ addons.add(CurrencyConverter())
+ addons.add(Dice())
+ addons.add(GoogleSearch())
+ addons.add(Info(tell))
+ addons.add(Joke())
+ addons.add(Lookup())
+ addons.add(Ping())
+ addons.add(RockPaperScissors())
+ addons.add(StockQuote())
+ addons.add(Weather2())
+ addons.add(WorldTime())
+ addons.add(War())
+
+ // Sort the addons
+ addons.names.sort()
+ }
+}
+
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/Pinboard.kt b/src/main/kotlin/net/thauvin/erik/mobibot/Pinboard.kt
new file mode 100644
index 0000000..9d46188
--- /dev/null
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/Pinboard.kt
@@ -0,0 +1,125 @@
+/*
+ * Pinboard.kt
+ *
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package net.thauvin.erik.mobibot
+
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import net.thauvin.erik.mobibot.entries.EntryLink
+import net.thauvin.erik.pinboard.PinboardPoster
+import java.time.ZoneId
+import java.time.ZonedDateTime
+import java.time.format.DateTimeFormatter
+import java.time.temporal.ChronoUnit
+import java.util.Date
+
+/**
+ * Handles posts to pinboard.in.
+ */
+class Pinboard {
+ private val poster = PinboardPoster()
+
+ /**
+ * Adds a pin.
+ */
+ 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.
+ */
+ fun deletePin(entry: EntryLink) {
+ if (poster.apiToken.isNotBlank()) {
+ runBlocking {
+ launch {
+ poster.deletePin(entry.link)
+ }
+ }
+ }
+ }
+
+ /**
+ * Updates a pin.
+ */
+ 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())
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Format a date to a UTC timestamp.
+ */
+ private fun Date.toTimestamp(): String {
+ return ZonedDateTime.ofInstant(
+ toInstant().truncatedTo(ChronoUnit.SECONDS),
+ ZoneId.systemDefault()
+ ).format(DateTimeFormatter.ISO_INSTANT)
+ }
+
+ /**
+ * Returns the pinboard.in extended attribution line.
+ */
+ private fun EntryLink.postedBy(ircServer: String): String {
+ return "Posted by $nick on $channel ( $ircServer )"
+ }
+}
+
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/TwitterOAuth.kt b/src/main/kotlin/net/thauvin/erik/mobibot/TwitterOAuth.kt
new file mode 100644
index 0000000..3df1c0a
--- /dev/null
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/TwitterOAuth.kt
@@ -0,0 +1,112 @@
+/*
+ * TwitterOAuth.kt
+ *
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package net.thauvin.erik.mobibot
+
+import twitter4j.TwitterException
+import twitter4j.TwitterFactory
+import twitter4j.auth.AccessToken
+import java.io.BufferedReader
+import java.io.IOException
+import java.io.InputStreamReader
+import kotlin.system.exitProcess
+
+/**
+ * The `TwitterOAuth` class.
+ *
+ * Go to [https://developer.twitter.com/en/apps](https://developer.twitter.com/en/apps) to register your bot.
+ *
+ * Then execute:
+ *
+ * `java -cp mobibot.jar net.thauvin.erik.mobibot.TwitterOAuth `
+ *
+ * and follow the prompts/instructions.
+ *
+ * @author [Erik C. Thauvin](https://erik.thauvin.net)
+ * @author [Twitter4J Example](https://twitter4j.org/en/code-examples.html#oauth)
+ */
+object TwitterOAuth {
+ /**
+ * Twitter OAuth Client Registration.
+ *
+ * @param args The consumerKey and consumerSecret should be passed as arguments.
+ */
+ @JvmStatic
+ @Throws(TwitterException::class, IOException::class)
+ fun main(args: Array) {
+ if (args.size == 2) {
+ with(TwitterFactory.getSingleton()) {
+ setOAuthConsumer(args[0], args[1])
+ val requestToken = oAuthRequestToken
+ var accessToken: AccessToken? = null
+ BufferedReader(InputStreamReader(System.`in`)).use { br ->
+ while (null == accessToken) {
+ print(
+ """
+ Open the following URL and grant access to your account:
+
+ ${requestToken.authorizationURL}
+
+ Enter the PIN (if available) or just hit enter. [PIN]: """.trimIndent()
+ )
+ val pin = br.readLine()
+ try {
+ accessToken = if (!pin.isNullOrEmpty()) {
+ getOAuthAccessToken(requestToken, pin)
+ } else {
+ oAuthAccessToken
+ }
+ println(
+ """
+ Please add the following to the bot's property file:
+
+ twitter-consumerKey=${args[0]}
+ twitter-consumerSecret=${args[1]}
+ twitter-token=${accessToken?.token}
+ twitter-tokenSecret=${accessToken?.tokenSecret}
+ """.trimIndent()
+ )
+ } catch (te: TwitterException) {
+ if (401 == te.statusCode) {
+ println("Unable to get the access token.")
+ } else {
+ te.printStackTrace()
+ }
+ }
+ }
+ }
+ }
+ } else {
+ println("Usage: ${TwitterOAuth::class.java.name} ")
+ }
+ exitProcess(0)
+ }
+}
diff --git a/src/main/java/net/thauvin/erik/mobibot/TwitterTimer.kt b/src/main/kotlin/net/thauvin/erik/mobibot/TwitterTimer.kt
similarity index 86%
rename from src/main/java/net/thauvin/erik/mobibot/TwitterTimer.kt
rename to src/main/kotlin/net/thauvin/erik/mobibot/TwitterTimer.kt
index f3a5450..09bfa46 100644
--- a/src/main/java/net/thauvin/erik/mobibot/TwitterTimer.kt
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/TwitterTimer.kt
@@ -1,7 +1,7 @@
/*
* TwitterTimer.kt
*
- * Copyright (c) 2004-2020, Erik C. Thauvin (erik@thauvin.net)
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -32,10 +32,11 @@
package net.thauvin.erik.mobibot
-import java.util.*
+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.twitterAutoPost(index)
+ twitter.postEntry(index)
}
}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/Utils.kt b/src/main/kotlin/net/thauvin/erik/mobibot/Utils.kt
new file mode 100644
index 0000000..b68ee47
--- /dev/null
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/Utils.kt
@@ -0,0 +1,391 @@
+/*
+ * Utils.kt
+ *
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package net.thauvin.erik.mobibot
+
+import net.thauvin.erik.mobibot.msg.Message
+import net.thauvin.erik.mobibot.msg.Message.Companion.DEFAULT_COLOR
+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
+import java.net.URL
+import java.net.URLEncoder
+import java.nio.charset.StandardCharsets
+import java.time.LocalDateTime
+import java.time.ZoneId
+import java.time.format.DateTimeFormatter
+import java.util.Date
+import java.util.Properties
+import java.util.stream.Collectors
+import kotlin.time.DurationUnit
+import kotlin.time.toDuration
+
+/**
+ * Miscellaneous utilities.
+ */
+@Suppress("TooManyFunctions")
+object Utils {
+ private val searchFlags = arrayOf("%c", "%n")
+
+ /**
+ * Appends a suffix to the end of the String if not present.
+ */
+ @JvmStatic
+ fun String.appendIfMissing(suffix: Char): String {
+ return if (last() != suffix) {
+ "$this${suffix}"
+ } else {
+ this
+ }
+ }
+
+ /**
+ * Makes the given int bold.
+ */
+ @JvmStatic
+ fun Int.bold(): String = toString().bold()
+
+ /**
+ * Makes the given long bold.
+ */
+ @JvmStatic
+ fun Long.bold(): String = toString().bold()
+
+ /**
+ * Makes the given string bold.
+ */
+ @JvmStatic
+ fun String?.bold(): String = colorize(Colors.BOLD)
+
+ /**
+ * Returns the [PircBotX] instance.
+ */
+ fun GenericMessageEvent.bot(): PircBotX {
+ return getBot() as PircBotX
+ }
+
+ /**
+ * Build a help command by replacing `%c` with the bot's pub/priv command, and `%n` with the bot's
+ * nick.
+ */
+ @JvmStatic
+ fun buildCmdSyntax(text: String, botNick: String, isPrivate: Boolean): String {
+ val replace = arrayOf(if (isPrivate) "/msg $botNick" else "$botNick:", botNick)
+ return text.replaceEach(searchFlags, replace)
+ }
+
+ /**
+ * Capitalize a string.
+ */
+ @JvmStatic
+ fun String.capitalise(): String = lowercase().replaceFirstChar { it.uppercase() }
+
+ /**
+ * Capitalize words
+ */
+ fun String.capitalizeWords(): String = split(" ").joinToString(" ") { it.capitalise() }
+
+
+ /**
+ * Colorize a string.
+ */
+ @JvmStatic
+ fun String?.colorize(color: String): String {
+ return if (isNullOrEmpty()) {
+ ""
+ } else if (color == DEFAULT_COLOR) {
+ this
+ } else if (Colors.BOLD == color || Colors.REVERSE == color) {
+ color + this + color
+ } else {
+ color + this + Colors.NORMAL
+ }
+ }
+
+ /**
+ * Makes the given string cyan.
+ */
+ @JvmStatic
+ fun String?.cyan(): String = colorize(Colors.CYAN)
+
+ /**
+ * URL encodes the given string.
+ */
+ @JvmStatic
+ fun encodeUrl(s: String): String = URLEncoder.encode(s, StandardCharsets.UTF_8)
+
+ /**
+ * Returns a property as an int.
+ */
+ @JvmStatic
+ fun Properties.getIntProperty(key: String, defaultValue: Int): Int {
+ return getProperty(key)?.toIntOrDefault(defaultValue) ?: defaultValue
+ }
+
+ /**
+ * Makes the given string green.
+ */
+ @JvmStatic
+ fun String?.green(): String = colorize(Colors.DARK_GREEN)
+
+ /**
+ * Returns a formatted help string.
+ */
+ @JvmStatic
+ @JvmOverloads
+ fun helpFormat(help: String, isBold: Boolean = true, isIndent: Boolean = true): String {
+ val s = if (isBold) help.bold() else help
+ return if (isIndent) s.prependIndent() else s
+ }
+
+ /**
+ * Returns {@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)
+ }
+
+ /**
+ * Returns the last item of a list of strings or empty if none.
+ */
+ @JvmStatic
+ fun List.lastOrEmpty(): String {
+ return if (this.size >= 2) {
+ this.last()
+ } else
+ ""
+ }
+
+ /**
+ * Returns {@code true} if the list does not contain the given string.
+ */
+ @JvmStatic
+ fun List.notContains(text: String, ignoreCase: Boolean = false) = this.none { it.equals(text, ignoreCase) }
+
+ /**
+ * Obfuscates the given string.
+ */
+ @JvmStatic
+ fun String.obfuscate(): String {
+ return if (isNotBlank()) {
+ "x".repeat(length)
+ } else this
+ }
+
+ /**
+ * Returns the plural form of a word, if count > 1.
+ */
+ @JvmStatic
+ fun String.plural(count: Long): String {
+ return if (count > 1) "${this}s" else this
+ }
+
+ /**
+ * Makes the given string red.
+ */
+ @JvmStatic
+ fun String?.red(): String = colorize(Colors.RED)
+
+ /**
+ * Replaces all occurrences of Strings within another String.
+ */
+ @JvmStatic
+ fun String.replaceEach(search: Array, replace: Array): String {
+ var result = this
+ if (search.size == replace.size) {
+ search.forEachIndexed { i, s ->
+ result = result.replace(s, replace[i])
+ }
+ }
+ return result
+ }
+
+ /**
+ * Makes the given string reverse color.
+ */
+ @JvmStatic
+ fun String?.reverseColor(): String = colorize(Colors.REVERSE)
+
+ /**
+ * Send a formatted commands/modules, etc. list.
+ */
+ @JvmStatic
+ fun GenericMessageEvent.sendList(
+ list: List,
+ maxPerLine: Int,
+ separator: String = " ",
+ isBold: Boolean = false,
+ isIndent: Boolean = false
+ ) {
+ var i = 0
+ while (i < list.size) {
+ sendMessage(
+ helpFormat(
+ list.subList(i, list.size.coerceAtMost(i + maxPerLine)).joinToString(separator, truncated = ""),
+ isBold,
+ isIndent
+ ),
+ )
+ i += maxPerLine
+ }
+ }
+
+ /**
+ * Sends a [message].
+ */
+ @JvmStatic
+ fun GenericMessageEvent.sendMessage(channel: String, message: Message) {
+ if (message.isNotice) {
+ bot().sendIRC().notice(user.nick, message.msg.colorize(message.color))
+ } else if (message.isPrivate || this is PrivateMessageEvent || channel.isBlank()) {
+ respondPrivateMessage(message.msg.colorize(message.color))
+ } else {
+ bot().sendIRC().message(channel, message.msg.colorize(message.color))
+ }
+ }
+
+ /**
+ * Sends a response as a private message or notice.
+ */
+ @JvmStatic
+ fun GenericMessageEvent.sendMessage(message: String) {
+ if (this is PrivateMessageEvent) {
+ respondPrivateMessage(message)
+ } else {
+ bot().sendIRC().notice(user.nick, message)
+ }
+ }
+
+ /**
+ * Returns today's date.
+ */
+ @JvmStatic
+ fun today(): String = LocalDateTime.now().toIsoLocalDate()
+
+ /**
+ * Converts a string to an int.
+ */
+ @JvmStatic
+ fun String.toIntOrDefault(defaultValue: Int): Int {
+ return try {
+ toInt()
+ } catch (e: NumberFormatException) {
+ defaultValue
+ }
+ }
+
+ /**
+ * Returns the specified date as an ISO local date string.
+ */
+ @JvmStatic
+ fun Date.toIsoLocalDate(): String {
+ return LocalDateTime.ofInstant(toInstant(), ZoneId.systemDefault()).toIsoLocalDate()
+ }
+
+ /**
+ * Returns the specified date as an ISO local date string.
+ */
+ @JvmStatic
+ fun LocalDateTime.toIsoLocalDate(): String = format(DateTimeFormatter.ISO_LOCAL_DATE)
+
+ /**
+ * Returns the specified date formatted as `yyyy-MM-dd HH:mm`.
+ */
+ @JvmStatic
+ fun Date.toUtcDateTime(): String {
+ return LocalDateTime.ofInstant(toInstant(), ZoneId.systemDefault()).toUtcDateTime()
+ }
+
+ /**
+ * Returns the specified date formatted as `yyyy-MM-dd HH:mm`.
+ */
+ @JvmStatic
+ fun LocalDateTime.toUtcDateTime(): String = format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))
+
+ /**
+ * Converts XML/XHTML entities to plain text.
+ */
+ @JvmStatic
+ fun unescapeXml(str: String): String = Jsoup.parse(str).text()
+
+ /**
+ * Converts milliseconds to year month week day hour and minutes.
+ */
+ @JvmStatic
+ fun uptime(uptime: Long): String {
+ uptime.toDuration(DurationUnit.MILLISECONDS).toComponents { wholeDays, hours, minutes, _, _ ->
+ val years = wholeDays / 365
+ var days = wholeDays % 365
+ val months = days / 30
+ days %= 30
+ val weeks = days / 7
+ days %= 7
+
+ with(StringBuffer()) {
+ if (years > 0) {
+ append(years).append(" year".plural(years)).append(' ')
+ }
+ if (months > 0) {
+ append(weeks).append(" month".plural(months)).append(' ')
+ }
+ if (weeks > 0) {
+ append(weeks).append(" week".plural(weeks)).append(' ')
+ }
+ if (days > 0) {
+ append(days).append(" day".plural(days)).append(' ')
+ }
+ if (hours > 0) {
+ append(hours).append(" hour".plural(hours.toLong())).append(' ')
+ }
+
+ append(minutes).append(" minute".plural(minutes.toLong()))
+
+ return toString()
+ }
+ }
+ }
+
+ /**
+ * Reads contents of a URL.
+ */
+ @JvmStatic
+ @Throws(IOException::class)
+ fun urlReader(url: URL): String {
+ BufferedReader(InputStreamReader(url.openStream(), StandardCharsets.UTF_8))
+ .use { reader -> return reader.lines().collect(Collectors.joining(System.lineSeparator())) }
+ }
+}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/commands/AbstractCommand.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/AbstractCommand.kt
new file mode 100644
index 0000000..674fe0f
--- /dev/null
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/AbstractCommand.kt
@@ -0,0 +1,83 @@
+/*
+ * AbstractCommand.kt
+ *
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package net.thauvin.erik.mobibot.commands
+
+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 {
+ abstract val name: String
+ abstract val help: List
+ abstract val isOpOnly: Boolean
+ abstract val isPublic: Boolean
+ abstract val isVisible: Boolean
+
+ val properties: MutableMap = ConcurrentHashMap()
+
+ abstract fun commandResponse(channel: String, args: String, event: GenericMessageEvent)
+
+ open fun helpResponse(channel: String, topic: String, event: GenericMessageEvent): Boolean {
+ if (!isOpOnly || isOpOnly == isChannelOp(channel, event)) {
+ for (h in help) {
+ event.sendMessage(
+ buildCmdSyntax(h, event.bot().nick, event is PrivateMessageEvent || !isPublic),
+ )
+ }
+ return true
+ }
+ return false
+ }
+
+ open fun initProperties(vararg keys: String) {
+ keys.forEach {
+ properties[it] = ""
+ }
+ }
+
+ open fun isEnabled(): Boolean {
+ return true
+ }
+
+ open fun matches(message: String): Boolean {
+ return false
+ }
+
+ open fun setProperty(key: String, value: String) {
+ properties[key] = value
+ }
+}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/commands/ChannelFeed.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/ChannelFeed.kt
new file mode 100644
index 0000000..cc80595
--- /dev/null
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/ChannelFeed.kt
@@ -0,0 +1,69 @@
+/*
+ * ChannelFeed.kt
+ *
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package net.thauvin.erik.mobibot.commands
+
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import net.thauvin.erik.mobibot.FeedReader
+import net.thauvin.erik.mobibot.Utils.helpFormat
+import org.pircbotx.hooks.types.GenericMessageEvent
+
+class ChannelFeed(channel: String) : AbstractCommand() {
+ override val name = channel
+ override val help = listOf("To list the last 5 posts from the channel's weblog feed:", helpFormat("%c $channel"))
+ override val isOpOnly = false
+ override val isPublic = true
+ override val isVisible = true
+
+ companion object {
+ const val FEED_PROP = "feed"
+ }
+
+ init {
+ initProperties(FEED_PROP)
+ }
+
+ override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
+ if (isEnabled()) {
+ runBlocking {
+ launch {
+ FeedReader(properties[FEED_PROP]!!, event).run()
+ }
+ }
+ }
+ }
+
+ override fun isEnabled(): Boolean {
+ return !properties[FEED_PROP].isNullOrBlank()
+ }
+}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/commands/Cycle.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Cycle.kt
new file mode 100644
index 0000000..8c6e777
--- /dev/null
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Cycle.kt
@@ -0,0 +1,64 @@
+/*
+ * Cycle.kt
+ *
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package net.thauvin.erik.mobibot.commands
+
+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 : AbstractCommand() {
+ private val wait = 10
+ override val name = "cycle"
+ override val help = listOf("To have the bot leave the channel and come back:", helpFormat("%c $name"))
+ override val isOpOnly = true
+ override val isPublic = false
+ override val isVisible = true
+
+ override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
+ with(event.bot()) {
+ if (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 {
+ helpResponse(channel, args, event)
+ }
+ }
+ }
+}
diff --git a/src/test/java/net/thauvin/erik/mobibot/modules/LookupTest.java b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Die.kt
similarity index 58%
rename from src/test/java/net/thauvin/erik/mobibot/modules/LookupTest.java
rename to src/main/kotlin/net/thauvin/erik/mobibot/commands/Die.kt
index d0d55f5..26f05c3 100644
--- a/src/test/java/net/thauvin/erik/mobibot/modules/LookupTest.java
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Die.kt
@@ -1,7 +1,7 @@
/*
- * LookupTest.java
+ * Die.kt
*
- * Copyright (c) 2004-2019, Erik C. Thauvin (erik@thauvin.net)
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -30,39 +30,37 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package net.thauvin.erik.mobibot.modules;
+package net.thauvin.erik.mobibot.commands
-import org.testng.annotations.Test;
+import net.thauvin.erik.mobibot.Utils.bot
+import net.thauvin.erik.mobibot.Utils.isChannelOp
+import org.pircbotx.hooks.types.GenericMessageEvent
-import java.util.Arrays;
+class Die : AbstractCommand() {
+ override val name = "die"
+ override val help = emptyList()
+ override val isOpOnly = true
+ override val isPublic = false
+ override val isVisible = false
-import static org.assertj.core.api.Assertions.assertThat;
-
-/**
- * The Lookup Test class.
- *
- * @author Erik C. Thauvin
- * @created 2017-05-30
- * @since 1.0
- */
-@SuppressWarnings("PMD.AvoidUsingHardCodedIP")
-public class LookupTest {
- @Test
- public void testLookupImpl() {
- AbstractModuleTest.testAbstractModule(new Lookup());
+ 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!")
+ }
+ }
}
- @Test
- public void testLookup() throws Exception {
- final String result = Lookup.lookup("erik.thauvin.net");
- assertThat(result).as("lookup(erik.thauvin.net/104.31.77.12)").contains("104.31.77.12");
+ companion object {
+ /**
+ * Max days property.
+ */
+ const val DIE_PROP = "die"
}
- @Test
- public void testWhois() throws Exception {
- final String[] result = Lookup.whois("17.178.96.59", Lookup.WHOIS_HOST);
-
- assertThat(Arrays.stream(result).anyMatch(m -> m.contains("Apple Inc.")))
- .as("whois(17.178.96.59/Apple Inc.").isTrue();
+ init {
+ initProperties(DIE_PROP)
}
}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/commands/Ignore.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Ignore.kt
new file mode 100644
index 0000000..afd9bf4
--- /dev/null
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Ignore.kt
@@ -0,0 +1,148 @@
+/*
+ * Ignore.kt
+ *
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package net.thauvin.erik.mobibot.commands
+
+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 : AbstractCommand() {
+ private val me = "me"
+
+ init {
+ initProperties(IGNORE_PROP)
+ }
+
+ override val name = IGNORE_CMD
+ override val help = listOf(
+ "To ignore a link posted to the channel:",
+ helpFormat("https://www.foo.bar %n"),
+ "To check your ignore status:",
+ helpFormat("%c $name"),
+ "To toggle your ignore status:",
+ helpFormat("%c $name $me")
+ )
+ private val helpOp = help.plus(
+ arrayOf("To add/remove nicks from the ignored list:", helpFormat("%c $name [ ...]"))
+ )
+
+ override val isOpOnly = false
+ override val isPublic = true
+ override val isVisible = true
+
+ companion object {
+ const val IGNORE_CMD = "ignore"
+ const val IGNORE_PROP = IGNORE_CMD
+ private val ignored = mutableSetOf()
+
+ @JvmStatic
+ fun isNotIgnored(nick: String): Boolean {
+ return !ignored.contains(nick.lowercase())
+ }
+ }
+
+ override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
+ val isMe = args.trim().equals(me, true)
+ if (isMe || !isChannelOp(channel, event)) {
+ val nick = event.user.nick.lowercase()
+ ignoreNick(nick, isMe, event)
+ } else {
+ ignoreOp(args, event)
+ }
+ }
+
+ override fun helpResponse(channel: String, topic: String, event: GenericMessageEvent): Boolean {
+ return if (isChannelOp(channel, event)) {
+ for (h in helpOp) {
+ event.sendMessage(buildCmdSyntax(h, event.bot().nick, true))
+ }
+ true
+ } else {
+ super.helpResponse(channel, topic, event)
+ }
+ }
+
+ private fun ignoreNick(sender: String, isMe: Boolean, event: GenericMessageEvent) {
+ if (isMe) {
+ if (ignored.remove(sender)) {
+ event.sendMessage("You are no longer ignored.")
+ } else {
+ ignored.add(sender)
+ event.sendMessage("You are now ignored.")
+ }
+ } else {
+ if (ignored.contains(sender)) {
+ event.sendMessage("You are currently ignored.")
+ } else {
+ event.sendMessage("You are not currently ignored.")
+ }
+ }
+ }
+
+ private fun ignoreOp(args: String, event: GenericMessageEvent) {
+ if (args.isNotEmpty()) {
+ val nicks = args.lowercase().split(" ")
+ for (nick in nicks) {
+ val ignore = if (me == nick) {
+ nick.lowercase()
+ } else {
+ nick
+ }
+ if (!ignored.remove(ignore)) {
+ ignored.add(ignore)
+ }
+ }
+ }
+
+ if (ignored.size > 0) {
+ event.sendMessage("The following nicks are ignored:")
+ event.sendList(ignored.sorted(), 8, isIndent = true)
+ } else {
+ event.sendMessage("No one is currently ${"ignored".bold()}.")
+ }
+ }
+
+ override fun setProperty(key: String, value: String) {
+ super.setProperty(key, value)
+ if (IGNORE_PROP == key) {
+ ignored.addAll(value.split(LinksMgr.TAG_MATCH.toRegex()))
+ }
+ }
+
+}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/commands/Info.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Info.kt
new file mode 100644
index 0000000..27521ef
--- /dev/null
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Info.kt
@@ -0,0 +1,76 @@
+/*
+ * Info.kt
+ *
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package net.thauvin.erik.mobibot.commands
+
+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(private val tell: Tell) : AbstractCommand() {
+ private val allVersions = listOf(
+ "${ReleaseInfo.PROJECT.capitalise()} ${ReleaseInfo.VERSION} (${ReleaseInfo.WEBSITE.green()})",
+ "Written by ${ReleaseInfo.AUTHOR} (${ReleaseInfo.AUTHOR_URL.green()})"
+ )
+ override val name = "info"
+ override val help = listOf("To view information about the bot:", helpFormat("%c $name"))
+ override val isOpOnly = false
+ override val isPublic = true
+ override val isVisible = true
+
+ 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(']')
+ event.sendMessage(info.toString())
+ }
+}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/commands/Me.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Me.kt
new file mode 100644
index 0000000..f1098fa
--- /dev/null
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Me.kt
@@ -0,0 +1,52 @@
+/*
+ * Me.kt
+ *
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package net.thauvin.erik.mobibot.commands
+
+import net.thauvin.erik.mobibot.Utils.bot
+import net.thauvin.erik.mobibot.Utils.helpFormat
+import net.thauvin.erik.mobibot.Utils.isChannelOp
+import org.pircbotx.hooks.types.GenericMessageEvent
+
+class Me : AbstractCommand() {
+ override val name = "me"
+ override val help = listOf("To have the bot perform an action:", helpFormat("%c $name "))
+ override val isOpOnly = true
+ override val isPublic = false
+ override val isVisible = true
+
+ override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
+ if (isChannelOp(channel, event)) {
+ event.bot().sendIRC().action(channel, args)
+ }
+ }
+}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/commands/Modules.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Modules.kt
new file mode 100644
index 0000000..82a0b06
--- /dev/null
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Modules.kt
@@ -0,0 +1,60 @@
+/*
+ * Modules.kt
+ *
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package net.thauvin.erik.mobibot.commands
+
+import net.thauvin.erik.mobibot.Utils.helpFormat
+import net.thauvin.erik.mobibot.Utils.isChannelOp
+import net.thauvin.erik.mobibot.Utils.sendList
+import org.pircbotx.hooks.types.GenericMessageEvent
+
+class Modules(private val modules: List) : AbstractCommand() {
+ override val name = "modules"
+ override val help = listOf("To view a list of enabled modules:", helpFormat("%c $name"))
+ override val isOpOnly = true
+ override val isPublic = false
+ override val isVisible = true
+
+ override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
+ if (isChannelOp(channel, event)) {
+ if (modules.isEmpty()) {
+ event.respondPrivateMessage("There are no enabled modules.")
+ } else {
+ event.respondPrivateMessage("The enabled modules are: ")
+ event.sendList(modules, 7, isIndent = true)
+ }
+ } else {
+ helpResponse(channel, args, event)
+ }
+ }
+}
+
diff --git a/src/main/java/net/thauvin/erik/mobibot/modules/ThreadedModule.java b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Msg.kt
similarity index 59%
rename from src/main/java/net/thauvin/erik/mobibot/modules/ThreadedModule.java
rename to src/main/kotlin/net/thauvin/erik/mobibot/commands/Msg.kt
index 7119950..8bd4b37 100644
--- a/src/main/java/net/thauvin/erik/mobibot/modules/ThreadedModule.java
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Msg.kt
@@ -1,7 +1,7 @@
/*
- * ThreadedModule.java
+ * Msg.kt
*
- * Copyright (c) 2004-2020, Erik C. Thauvin (erik@thauvin.net)
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -30,36 +30,32 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package net.thauvin.erik.mobibot.modules;
+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
-/**
- * The ThreadedModule class.
- *
- * @author Erik C. Thauvin
- * @created 2019-04-03
- * @since 1.0
- */
-public abstract class ThreadedModule extends AbstractModule {
- /**
- * {@inheritDoc}
- */
- @Override
- public void commandResponse(final Mobibot bot,
- final String sender,
- final String cmd,
- final String args,
- final boolean isPrivate) {
- if (isEnabled() && args.length() > 0) {
- new Thread(() -> run(bot, sender, cmd, args)).start();
- } else {
- helpResponse(bot, sender, args, isPrivate);
+class Msg : AbstractCommand() {
+ override val name = "msg"
+ override val help = listOf(
+ "To have the bot send a private message to someone:",
+ helpFormat("%c $name ")
+ )
+ override val isOpOnly = true
+ override val isPublic = false
+ override val isVisible = true
+
+ override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
+ if (isChannelOp(channel, event)) {
+ val msg = args.split(" ", limit = 2)
+ if (args.length > 2) {
+ event.bot().sendIRC().message(msg[0], msg[1])
+ event.respondPrivateMessage("A message was sent to ${msg[0]}")
+ } else {
+ helpResponse(channel, args, event)
+ }
}
}
-
- /**
- * Runs the thread.
- */
- abstract void run(Mobibot bot, String sender, @SuppressWarnings("unused") String cmd, String args);
}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/commands/Nick.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Nick.kt
new file mode 100644
index 0000000..c864763
--- /dev/null
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Nick.kt
@@ -0,0 +1,52 @@
+/*
+ * Nick.kt
+ *
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package net.thauvin.erik.mobibot.commands
+
+import net.thauvin.erik.mobibot.Utils.bot
+import net.thauvin.erik.mobibot.Utils.helpFormat
+import net.thauvin.erik.mobibot.Utils.isChannelOp
+import org.pircbotx.hooks.types.GenericMessageEvent
+
+class Nick : AbstractCommand() {
+ override val name = "nick"
+ override val help = listOf("To change the bot's nickname:", helpFormat("%c $name "))
+ override val isOpOnly = true
+ override val isPublic = true
+ override val isVisible = true
+
+ override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
+ if (isChannelOp(channel, event)) {
+ event.bot().sendIRC().changeNick(args)
+ }
+ }
+}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/commands/Recap.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Recap.kt
new file mode 100644
index 0000000..3647bcc
--- /dev/null
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Recap.kt
@@ -0,0 +1,82 @@
+/*
+ * Recap.kt
+ *
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package net.thauvin.erik.mobibot.commands
+
+import net.thauvin.erik.mobibot.Utils.helpFormat
+import net.thauvin.erik.mobibot.Utils.sendMessage
+import net.thauvin.erik.mobibot.Utils.toUtcDateTime
+import org.pircbotx.hooks.types.GenericMessageEvent
+import java.time.Clock
+import java.time.LocalDateTime
+
+class Recap : AbstractCommand() {
+ override val name = "recap"
+ override val help = listOf(
+ "To list the last 10 public channel messages:",
+ helpFormat("%c $name")
+ )
+ override val isOpOnly = false
+ override val isPublic = true
+ override val isVisible = true
+
+ companion object {
+ const val MAX_RECAPS = 10
+
+ @JvmField
+ val recaps = mutableListOf()
+
+ /**
+ * Stores the last 10 public messages and actions.
+ */
+ @JvmStatic
+ fun storeRecap(sender: String, message: String, isAction: Boolean) {
+ recaps.add(
+ LocalDateTime.now(Clock.systemUTC()).toUtcDateTime()
+ + " - $sender" + (if (isAction) " " else ": ") + message
+ )
+ if (recaps.size > MAX_RECAPS) {
+ recaps.removeFirst()
+ }
+ }
+ }
+
+ override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
+ if (recaps.isNotEmpty()) {
+ for (r in recaps) {
+ event.sendMessage(r)
+ }
+ } else {
+ event.sendMessage("Sorry, nothing to recap.")
+ }
+ }
+}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/commands/Say.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Say.kt
new file mode 100644
index 0000000..c03a221
--- /dev/null
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Say.kt
@@ -0,0 +1,52 @@
+/*
+ * Say.kt
+ *
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package net.thauvin.erik.mobibot.commands
+
+import net.thauvin.erik.mobibot.Utils.bot
+import net.thauvin.erik.mobibot.Utils.helpFormat
+import net.thauvin.erik.mobibot.Utils.isChannelOp
+import org.pircbotx.hooks.types.GenericMessageEvent
+
+class Say : AbstractCommand() {
+ override val name = "say"
+ override val help = listOf("To have the bot say something on the channel:", helpFormat("%c $name "))
+ override val isOpOnly = true
+ override val isPublic = false
+ override val isVisible = true
+
+ override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
+ if (isChannelOp(channel, event)) {
+ event.bot().sendIRC().message(channel, args)
+ }
+ }
+}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/commands/Users.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Users.kt
new file mode 100644
index 0000000..fddb9be
--- /dev/null
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Users.kt
@@ -0,0 +1,59 @@
+/*
+ * Users.kt
+ *
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package net.thauvin.erik.mobibot.commands
+
+import net.thauvin.erik.mobibot.Utils.bot
+import net.thauvin.erik.mobibot.Utils.helpFormat
+import net.thauvin.erik.mobibot.Utils.sendList
+import org.pircbotx.hooks.types.GenericMessageEvent
+
+class Users : AbstractCommand() {
+ override val name = "users"
+ override val help = listOf("To list the users present on the channel:", helpFormat("%c $name"))
+ override val isOpOnly = false
+ override val isPublic = true
+ override val isVisible = true
+
+ override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
+ val nicks = mutableListOf()
+ val ch = event.bot().userChannelDao.getChannel(channel)
+ ch.users.forEach {
+ if (it.channelsOpIn.contains(ch)) {
+ nicks.add("@${it.nick}")
+ } else {
+ nicks.add(it.nick)
+ }
+ }
+ event.sendList(nicks, 8)
+ }
+}
diff --git a/src/main/java/net/thauvin/erik/mobibot/msg/ErrorMessage.java b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Versions.kt
similarity index 54%
rename from src/main/java/net/thauvin/erik/mobibot/msg/ErrorMessage.java
rename to src/main/kotlin/net/thauvin/erik/mobibot/commands/Versions.kt
index dea79a6..2875e06 100644
--- a/src/main/java/net/thauvin/erik/mobibot/msg/ErrorMessage.java
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Versions.kt
@@ -1,7 +1,7 @@
/*
- * ErrorMessage.java
+ * Versions.kt
*
- * Copyright (c) 2004-2019, Erik C. Thauvin (erik@thauvin.net)
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -29,41 +29,31 @@
* 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
-package net.thauvin.erik.mobibot.msg;
+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
-/**
- * The ErrorMessage class.
- *
- * @author Erik C. Thauvin
- * @created 2019-04-07
- * @since 1.0
- */
-public class ErrorMessage extends Message {
- /**
- * Creates a new error message.
- *
- * @param message The error message.
- */
- public ErrorMessage(final String message) {
- super();
- this.setMessage(message);
- this.setError(true);
- this.setNotice(true);
- }
+class Versions : AbstractCommand() {
+ private val allVersions = listOf(
+ "Version: ${ReleaseInfo.VERSION} (${ReleaseInfo.BUILDDATE.toIsoLocalDate()})",
+ "Platform: " + System.getProperty("os.name") + ' ' + System.getProperty("os.version")
+ + " (" + System.getProperty("os.arch") + ')',
+ "Runtime: " + System.getProperty("java.runtime.name") + ' ' + System.getProperty("java.runtime.version")
+ )
+ override val name = "versions"
+ override val help = listOf("To view the versions data (bot, platform, java, etc.):", helpFormat("%c $name"))
+ override val isOpOnly = true
+ override val isPublic = false
+ override val isVisible = true
- /**
- * Creates a new error message.
- *
- * @param message The message.
- * @param color The message color.
- */
- @SuppressWarnings("unused")
- public ErrorMessage(final String message, final String color) {
- super();
- this.setMessage(message);
- this.setError(true);
- this.setNotice(true);
- this.setColor(color);
+ override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
+ if (isChannelOp(channel, event)) {
+ event.sendList(allVersions, 1)
+ }
}
}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/commands/links/Comment.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/links/Comment.kt
new file mode 100644
index 0000000..59f31c8
--- /dev/null
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/links/Comment.kt
@@ -0,0 +1,152 @@
+/*
+ * Comment.kt
+ *
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package net.thauvin.erik.mobibot.commands.links
+
+import net.thauvin.erik.mobibot.Constants
+import net.thauvin.erik.mobibot.Utils.bold
+import net.thauvin.erik.mobibot.Utils.helpFormat
+import net.thauvin.erik.mobibot.Utils.isChannelOp
+import net.thauvin.erik.mobibot.Utils.sendMessage
+import net.thauvin.erik.mobibot.commands.AbstractCommand
+import net.thauvin.erik.mobibot.entries.EntriesUtils.buildComment
+import net.thauvin.erik.mobibot.entries.EntriesUtils.buildLinkLabel
+import net.thauvin.erik.mobibot.entries.EntryLink
+import org.pircbotx.hooks.types.GenericMessageEvent
+
+class Comment : AbstractCommand() {
+ override val name = COMMAND
+ override val help = listOf(
+ "To add a comment:",
+ helpFormat("${Constants.LINK_CMD}1:This is a comment"),
+ "I will reply with a label, for example: ${Constants.LINK_CMD.bold()}1.1",
+ "To edit a comment, use its label: ",
+ helpFormat("${Constants.LINK_CMD}1.1:This is an edited comment"),
+ "To delete a comment, use its label and a minus sign: ",
+ helpFormat("${Constants.LINK_CMD}1.1:-")
+ )
+ override val isOpOnly = false
+ override val isPublic = true
+ override val isVisible = true
+
+ companion object {
+ const val COMMAND = "comment"
+ }
+
+ override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
+ val cmds = args.substring(1).split("[.:]".toRegex(), 3)
+ val entryIndex = cmds[0].toInt() - 1
+
+ if (entryIndex < 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(entry, entryIndex, commentIndex, event) // L1.1:
+ "-" -> deleteComment(channel, entry, entryIndex, commentIndex, event) // L1.1:-
+ else -> {
+ if (cmd.startsWith('?')) { // L1.1:?
+ changeAuthor(channel, cmd, entry, entryIndex, commentIndex, event)
+ } else { // L1.1:
+ setComment(cmd, entry, entryIndex, commentIndex, event)
+ }
+ }
+ }
+ }
+ }
+ }
+
+ override fun helpResponse(channel: String, topic: String, event: GenericMessageEvent): Boolean {
+ if (super.helpResponse(channel, topic, event)) {
+ if (isChannelOp(channel, event)) {
+ event.sendMessage("To change a comment's author:")
+ event.sendMessage(helpFormat("${Constants.LINK_CMD}1.1:?"))
+ }
+ return true
+ }
+ return false
+ }
+
+ override fun matches(message: String): Boolean {
+ return message.matches("^${Constants.LINK_CMD}[0-9]+\\.[0-9]+:.*".toRegex())
+ }
+
+ private fun changeAuthor(
+ channel: String,
+ cmd: String,
+ entry: EntryLink,
+ entryIndex: Int,
+ commentIndex: Int,
+ event: GenericMessageEvent
+ ) {
+ if (isChannelOp(channel, event) && cmd.length > 1) {
+ val comment = entry.getComment(commentIndex)
+ comment.nick = cmd.substring(1)
+ event.sendMessage(buildComment(entryIndex, commentIndex, comment))
+ LinksMgr.entries.save()
+ } else {
+ event.sendMessage("Please ask a channel op to change the author of this comment for you.")
+ }
+ }
+
+ private fun deleteComment(
+ channel: String,
+ entry: EntryLink,
+ entryIndex: Int,
+ commentIndex: Int,
+ event: GenericMessageEvent
+ ) {
+ if (isChannelOp(channel, event) || event.user.nick == entry.getComment(commentIndex).nick) {
+ entry.deleteComment(commentIndex)
+ event.sendMessage("Comment ${buildLinkLabel(entryIndex)}.${commentIndex + 1} removed.")
+ LinksMgr.entries.save()
+ } else {
+ event.sendMessage("Please ask a channel op to delete this comment for you.")
+ }
+ }
+
+ private fun setComment(
+ cmd: String,
+ entry: EntryLink,
+ entryIndex: Int,
+ commentIndex: Int,
+ event: GenericMessageEvent
+ ) {
+ entry.setComment(commentIndex, cmd, event.user.nick)
+ event.sendMessage(buildComment(entryIndex, commentIndex, entry.getComment(commentIndex)))
+ LinksMgr.entries.save()
+ }
+
+ private fun showComment(entry: EntryLink, entryIndex: Int, commentIndex: Int, event: GenericMessageEvent) {
+ event.sendMessage(buildComment(entryIndex, commentIndex, entry.getComment(commentIndex)))
+ }
+}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/commands/links/LinksMgr.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/links/LinksMgr.kt
new file mode 100644
index 0000000..8485be0
--- /dev/null
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/links/LinksMgr.kt
@@ -0,0 +1,208 @@
+/*
+ * LinksMgr.kt
+ *
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package net.thauvin.erik.mobibot.commands.links
+
+import net.thauvin.erik.mobibot.Constants
+import net.thauvin.erik.mobibot.Pinboard
+import net.thauvin.erik.mobibot.Utils.bold
+import net.thauvin.erik.mobibot.Utils.bot
+import net.thauvin.erik.mobibot.Utils.helpFormat
+import net.thauvin.erik.mobibot.Utils.sendMessage
+import net.thauvin.erik.mobibot.Utils.today
+import net.thauvin.erik.mobibot.commands.AbstractCommand
+import net.thauvin.erik.mobibot.commands.Ignore.Companion.isNotIgnored
+import net.thauvin.erik.mobibot.entries.Entries
+import net.thauvin.erik.mobibot.entries.EntriesUtils.buildLink
+import net.thauvin.erik.mobibot.entries.EntriesUtils.buildLinkLabel
+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 : AbstractCommand() {
+ private val defaultTags: MutableList = mutableListOf()
+ private val keywords: MutableList = mutableListOf()
+
+ override val name = Constants.LINK_CMD
+ override val help = emptyList()
+ override val isOpOnly = false
+ override val isPublic = false
+ override val isVisible = false
+
+ init {
+ initProperties(TAGS_PROP, KEYWORDS_PROP)
+ }
+
+ companion object {
+ const val LINK_MATCH = "^[hH][tT][tT][pP](|[sS])://.*"
+ const val KEYWORDS_PROP = "tags-keywords"
+ const val TAGS_PROP = "tags"
+ const val TAG_MATCH = ", *| +"
+
+ /**
+ * Entries array
+ */
+ @JvmField
+ val entries = Entries()
+
+ /**
+ * Pinboard handler.
+ */
+ @JvmField
+ val pinboard = Pinboard()
+
+ /**
+ * Twitter handler.
+ */
+ @JvmField
+ val twitter = Twitter()
+
+ /**
+ * Let the user know if the entries are too old to be modified.
+ */
+ @JvmStatic
+ fun isUpToDate(event: GenericMessageEvent): Boolean {
+ if (entries.lastPubDate != today()) {
+ event.sendMessage("The links are too old to be updated.")
+ return false
+ }
+ return true
+ }
+ }
+
+ override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
+ val cmds = args.split(" ".toRegex(), 2)
+ val sender = event.user.nick
+ val botNick = event.bot().nick
+ val login = event.user.login
+
+ if (isNotIgnored(sender) && (cmds.size == 1 || !cmds[1].contains(botNick))) {
+ val link = cmds[0].trim()
+ if (!isDupEntry(link, event)) {
+ var title = ""
+ val tags = ArrayList(defaultTags)
+ if (cmds.size == 2) {
+ val data = cmds[1].trim().split("${Tags.COMMAND}:", limit = 2)
+ title = data[0].trim()
+ if (data.size > 1) {
+ tags.addAll(data[1].split(TAG_MATCH.toRegex()))
+ }
+ }
+
+ if (title.isBlank()) {
+ title = fetchTitle(link)
+ }
+
+ if (title != Constants.NO_TITLE) {
+ matchTagKeywords(title, tags)
+ }
+
+ // Links are old, clear them
+ if (entries.lastPubDate != today()) {
+ entries.links.clear()
+ }
+
+ val entry = EntryLink(link, title, sender, login, channel, tags)
+ entries.links.add(entry)
+ val index = entries.links.lastIndexOf(entry)
+ event.sendMessage(buildLink(index, entry))
+
+ pinboard.addPin(event.bot().serverHostname, entry)
+
+ // Queue link for posting to Twitter.
+ twitter.queueEntry(index)
+
+ entries.save()
+
+ if (Constants.NO_TITLE == entry.title) {
+ event.sendMessage("Please specify a title, by typing:")
+ event.sendMessage(helpFormat("${buildLinkLabel(index)}:|This is the title"))
+ }
+ }
+ }
+ }
+
+ override fun helpResponse(channel: String, topic: String, event: GenericMessageEvent): Boolean = false
+
+ override fun matches(message: String): Boolean {
+ return message.matches(LINK_MATCH.toRegex())
+ }
+
+ internal fun fetchTitle(link: String): String {
+ try {
+ val html = Jsoup.connect(link)
+ .userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:74.0) Gecko/20100101 Firefox/74.0")
+ .get()
+ val title = html.title()
+ if (title.isNotBlank()) {
+ return title
+ }
+ } catch (ignore: IOException) {
+ // Do nothing
+ }
+ return Constants.NO_TITLE
+ }
+
+ private fun isDupEntry(link: String, event: GenericMessageEvent): Boolean {
+ synchronized(entries) {
+ return try {
+ val match = entries.links.single { it.link == link }
+ event.sendMessage(
+ "Duplicate".bold() + " >> " + buildLink(entries.links.indexOf(match), match)
+ )
+ true
+ } catch (ignore: NoSuchElementException) {
+ false
+ }
+ }
+ }
+
+ internal fun matchTagKeywords(title: String, tags: MutableList) {
+ for (match in keywords) {
+ val m = Regex.escape(match)
+ if (title.matches("(?i).*\\b$m\\b.*".toRegex())) {
+ tags.add(match)
+ }
+ }
+ }
+
+ override fun setProperty(key: String, value: String) {
+ super.setProperty(key, value)
+ if (KEYWORDS_PROP == key) {
+ keywords.addAll(value.split(TAG_MATCH.toRegex()))
+ } else if (TAGS_PROP == key) {
+ defaultTags.addAll(value.split(TAG_MATCH.toRegex()))
+ }
+ }
+}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/commands/links/Posting.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/links/Posting.kt
new file mode 100644
index 0000000..670daeb
--- /dev/null
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/links/Posting.kt
@@ -0,0 +1,164 @@
+/*
+ * Posting.kt
+ *
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package net.thauvin.erik.mobibot.commands.links
+
+import net.thauvin.erik.mobibot.Constants
+import net.thauvin.erik.mobibot.Utils.bold
+import net.thauvin.erik.mobibot.Utils.bot
+import net.thauvin.erik.mobibot.Utils.helpFormat
+import net.thauvin.erik.mobibot.Utils.isChannelOp
+import net.thauvin.erik.mobibot.Utils.sendMessage
+import net.thauvin.erik.mobibot.commands.AbstractCommand
+import net.thauvin.erik.mobibot.commands.links.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 : AbstractCommand() {
+ override val name = "posting"
+ override val help = listOf(
+ "Post a URL, by saying it on a line on its own:",
+ helpFormat(" [] ${Tags.COMMAND}: <+tag> [...]]"),
+ "I will reply with a label, for example: ${Constants.LINK_CMD.bold()}1",
+ "To add a title, use its label and a pipe:",
+ helpFormat("${Constants.LINK_CMD}1:|This is the title"),
+ "To add a comment:",
+ helpFormat("${Constants.LINK_CMD}1:This is a comment"),
+ "I will reply with a label, for example: ${Constants.LINK_CMD.bold()}1.1",
+ "To edit a comment, see: ",
+ helpFormat("%c ${Constants.HELP_CMD} ${Comment.COMMAND}")
+ )
+ override val isOpOnly = false
+ override val isPublic = true
+ override val isVisible = true
+
+ override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
+ val cmds = args.substring(1).split(":", limit = 2)
+ val entryIndex = cmds[0].toInt() - 1
+
+ if (entryIndex < entries.links.size) {
+ val cmd = cmds[1].trim()
+ if (cmd.isBlank()) {
+ showEntry(entryIndex, event) // L1:
+ } else if (LinksMgr.isUpToDate(event)) {
+ if (cmd == "-") {
+ removeEntry(channel, entryIndex, event) // L1:-
+ } else {
+ when (cmd[0]) {
+ '|' -> changeTitle(cmd, entryIndex, event) // L1:|
+ '=' -> changeUrl(channel, cmd, entryIndex, event) // L1:=
+ '?' -> changeAuthor(channel, cmd, entryIndex, event) // L1:?
+ else -> addComment(cmd, entryIndex, event) // L1:
+ }
+ }
+ }
+ }
+ }
+
+ override fun matches(message: String): Boolean {
+ return message.matches("${Constants.LINK_CMD}[0-9]+:.*".toRegex())
+ }
+
+ private fun addComment(cmd: String, entryIndex: Int, event: GenericMessageEvent) {
+ val entry: EntryLink = entries.links[entryIndex]
+ val commentIndex = entry.addComment(cmd, event.user.nick)
+ val comment = entry.getComment(commentIndex)
+ event.sendMessage(EntriesUtils.buildComment(entryIndex, commentIndex, comment))
+ entries.save()
+ }
+
+ private fun changeTitle(cmd: String, entryIndex: Int, event: GenericMessageEvent) {
+ if (cmd.length > 1) {
+ val entry: EntryLink = entries.links[entryIndex]
+ entry.title = cmd.substring(1).trim()
+ LinksMgr.pinboard.updatePin(event.bot().serverHostname, entry.link, entry)
+ event.sendMessage(EntriesUtils.buildLink(entryIndex, entry))
+ entries.save()
+ }
+ }
+
+ private fun changeUrl(channel: String, cmd: String, entryIndex: Int, event: GenericMessageEvent) {
+ val entry: EntryLink = entries.links[entryIndex]
+ if (entry.login == event.user.login || isChannelOp(channel, event)) {
+ val link = cmd.substring(1)
+ if (link.matches(LinksMgr.LINK_MATCH.toRegex())) {
+ val oldLink = entry.link
+ entry.link = link
+ LinksMgr.pinboard.updatePin(event.bot().serverHostname, oldLink, entry)
+ event.sendMessage(EntriesUtils.buildLink(entryIndex, entry))
+ entries.save()
+ }
+ }
+ }
+
+ private fun changeAuthor(channel: String, cmd: String, index: Int, event: GenericMessageEvent) {
+ if (isChannelOp(channel, event)) {
+ if (cmd.length > 1) {
+ val entry: EntryLink = entries.links[index]
+ entry.nick = cmd.substring(1)
+ LinksMgr.pinboard.updatePin(event.bot().serverHostname, entry.link, entry)
+ event.sendMessage(EntriesUtils.buildLink(index, entry))
+ entries.save()
+ }
+ } else {
+ event.sendMessage("Please ask a channel op to change the author of this link for you.")
+ }
+ }
+
+ private fun removeEntry(channel: String, index: Int, event: GenericMessageEvent) {
+ val entry: EntryLink = entries.links[index]
+ if (entry.login == event.user.login || 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 {
+ event.sendMessage("Please ask a channel op to remove this entry for you.")
+ }
+ }
+
+ private fun showEntry(index: Int, event: GenericMessageEvent) {
+ val entry: EntryLink = entries.links[index]
+ event.sendMessage(EntriesUtils.buildLink(index, entry))
+ if (entry.tags.isNotEmpty()) {
+ event.sendMessage(EntriesUtils.buildTags(index, entry))
+ }
+ if (entry.comments.isNotEmpty()) {
+ val comments = entry.comments
+ for (i in comments.indices) {
+ event.sendMessage(EntriesUtils.buildComment(index, i, comments[i]))
+ }
+ }
+ }
+}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/commands/links/Tags.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/links/Tags.kt
new file mode 100644
index 0000000..7c0d904
--- /dev/null
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/links/Tags.kt
@@ -0,0 +1,88 @@
+/*
+ * Tags.kt
+ *
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package net.thauvin.erik.mobibot.commands.links
+
+import net.thauvin.erik.mobibot.Constants
+import net.thauvin.erik.mobibot.Utils.bot
+import net.thauvin.erik.mobibot.Utils.helpFormat
+import net.thauvin.erik.mobibot.Utils.isChannelOp
+import net.thauvin.erik.mobibot.Utils.sendMessage
+import net.thauvin.erik.mobibot.commands.AbstractCommand
+import net.thauvin.erik.mobibot.entries.EntriesUtils
+import net.thauvin.erik.mobibot.entries.EntryLink
+import org.pircbotx.hooks.types.GenericMessageEvent
+
+class Tags : AbstractCommand() {
+ override val name = COMMAND
+ override val help = listOf(
+ "To categorize or tag a URL, use its label and a ${Constants.TAG_CMD}:",
+ helpFormat("${Constants.LINK_CMD}1${Constants.TAG_CMD}:<+tag|-tag> [...]")
+ )
+ override val isOpOnly = false
+ override val isPublic = true
+ override val isVisible = true
+
+ companion object {
+ const val COMMAND = "tags"
+ }
+
+ override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
+ val cmds = args.substring(1).split("${Constants.TAG_CMD}:", limit = 2)
+ val index = cmds[0].toInt() - 1
+
+ if (index < LinksMgr.entries.links.size && LinksMgr.isUpToDate(event)) {
+ val cmd = cmds[1].trim()
+ val entry: EntryLink = LinksMgr.entries.links[index]
+ if (cmd.isNotEmpty()) {
+ if (entry.login == event.user.login || isChannelOp(channel, event)) {
+ entry.setTags(cmd)
+ LinksMgr.pinboard.updatePin(event.bot().serverHostname, entry.link, entry)
+ event.sendMessage(EntriesUtils.buildTags(index, entry))
+ LinksMgr.entries.save()
+ } else {
+ event.sendMessage("Please ask a channel op to change the tags for you.")
+ }
+ } else {
+ if (entry.tags.isNotEmpty()) {
+ event.sendMessage(EntriesUtils.buildTags(index, entry))
+ } else {
+ event.sendMessage("The entry has no tags. Why don't add some?")
+ }
+ }
+ }
+ }
+
+ override fun matches(message: String): Boolean {
+ return message.matches("^${Constants.LINK_CMD}[0-9]+${Constants.TAG_CMD}:.*".toRegex())
+ }
+}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/commands/links/View.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/links/View.kt
new file mode 100644
index 0000000..f853cae
--- /dev/null
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/links/View.kt
@@ -0,0 +1,125 @@
+/*
+ * View.kt
+ *
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package net.thauvin.erik.mobibot.commands.links
+
+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.lastOrEmpty
+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 : AbstractCommand() {
+ override val name = VIEW_CMD
+ override val help = listOf(
+ "To list or search the current URL posts:",
+ helpFormat("%c $name [] []")
+ )
+ override val isOpOnly = false
+ override val isPublic = true
+ override val isVisible = true
+
+ companion object {
+ const val MAX_ENTRIES = 6
+ const val VIEW_CMD = "view"
+ }
+
+ override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
+ if (entries.links.isNotEmpty()) {
+ val p = parseArgs(args)
+ showPosts(p.first, p.second, event)
+ } else {
+ event.sendMessage("There is currently nothing to view. Why don't you post something?")
+ }
+ }
+
+ internal fun parseArgs(args: String): Pair {
+ var query = args.lowercase().trim()
+ var start = 0
+ if (query.isEmpty() && entries.links.size > MAX_ENTRIES) {
+ start = entries.links.size - MAX_ENTRIES
+ }
+ if (query.matches("^\\d+(| .*)".toRegex())) { // view [] []
+ val split = query.split(" ", limit = 2)
+ try {
+ start = split[0].toInt() - 1
+ query = split.lastOrEmpty().trim()
+ if (start > entries.links.size) {
+ start = 0
+ }
+ } catch (ignore: NumberFormatException) {
+ // Do nothing
+ }
+ }
+ return Pair(start, query)
+ }
+
+ private fun showPosts(start: Int, query: String, event: GenericMessageEvent) {
+ var index = start
+ var entry: EntryLink
+ var sent = 0
+ while (index < entries.links.size && sent < MAX_ENTRIES) {
+ entry = entries.links[index]
+ if (query.isNotBlank()) {
+ if (entry.matches(query)) {
+ event.sendMessage(EntriesUtils.buildLink(index, entry, true))
+ sent++
+ }
+ } else {
+ event.sendMessage(EntriesUtils.buildLink(index, entry, true))
+ sent++
+ }
+ index++
+ if (sent == MAX_ENTRIES && index < entries.links.size) {
+ event.sendMessage("To view more, try: ")
+ event.sendMessage(
+ helpFormat(
+ buildCmdSyntax(
+ "%c $name ${index + 1} $query",
+ event.bot().nick,
+ event is PrivateMessageEvent
+ )
+ )
+ )
+ }
+ }
+ if (sent == 0) {
+ event.sendMessage("No matches. Please try again.")
+ }
+ }
+}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/commands/tell/Tell.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/tell/Tell.kt
new file mode 100644
index 0000000..1100c99
--- /dev/null
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/tell/Tell.kt
@@ -0,0 +1,303 @@
+/*
+ * Tell.kt
+ *
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package net.thauvin.erik.mobibot.commands.tell
+
+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.plural
+import net.thauvin.erik.mobibot.Utils.reverseColor
+import net.thauvin.erik.mobibot.Utils.sendMessage
+import net.thauvin.erik.mobibot.Utils.toIntOrDefault
+import net.thauvin.erik.mobibot.Utils.toUtcDateTime
+import net.thauvin.erik.mobibot.commands.AbstractCommand
+import net.thauvin.erik.mobibot.commands.links.View
+import org.pircbotx.PircBotX
+import org.pircbotx.hooks.events.MessageEvent
+import org.pircbotx.hooks.types.GenericMessageEvent
+import org.pircbotx.hooks.types.GenericUserEvent
+
+/**
+ * The `Tell` command.
+ */
+class Tell(private val serialObject: String) : AbstractCommand() {
+ // Messages queue
+ private val messages: MutableList = mutableListOf()
+
+ // Maximum number of days to keep messages
+ private var maxDays = 7
+
+ // Message maximum queue size
+ private var maxSize = 50
+
+ /**
+ * The tell command.
+ */
+ override val name = "tell"
+
+ override val help = listOf(
+ "To send a message to someone when they join the channel:",
+ helpFormat("%c $name "),
+ "To view queued and sent messages:",
+ helpFormat("%c $name ${View.VIEW_CMD}"),
+ "Messages are kept for ${maxDays.bold()}" + " day".plural(maxDays.toLong()) + '.'
+ )
+ override val isOpOnly: Boolean = false
+ override val isPublic: Boolean = isEnabled()
+ override val isVisible: Boolean = isEnabled()
+
+ /**
+ * Cleans the messages queue.
+ */
+ private fun clean(): Boolean {
+ return TellMessagesMgr.clean(messages, maxDays.toLong())
+ }
+
+ override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
+ if (isEnabled()) {
+ if (args.isBlank()) {
+ helpResponse(channel, args, event)
+ } else if (args.startsWith(View.VIEW_CMD)) {
+ if (isChannelOp(channel, event) && "${View.VIEW_CMD} $TELL_ALL_KEYWORD" == args) {
+ viewAll(event)
+ } else {
+ viewMessages(event)
+ }
+ } else if (args.startsWith("$TELL_DEL_KEYWORD ")) {
+ deleteMessage(channel, args, event)
+ } else {
+ newMessage(channel, args, event)
+ }
+ if (clean()) {
+ save()
+ }
+ }
+ }
+
+ // Delete message.
+ private fun deleteMessage(channel: String, args: String, event: GenericMessageEvent) {
+ val split = args.split(" ")
+ if (split.size == 2) {
+ val id = split[1]
+ if (TELL_ALL_KEYWORD.equals(id, ignoreCase = true)) {
+ if (messages.removeIf { it.sender.equals(event.user.nick, true) && it.isReceived }) {
+ save()
+ event.sendMessage("Delivered messages have been deleted.")
+ } else {
+ event.sendMessage("No delivered messages were found.")
+ }
+ } else {
+ if (messages.removeIf {
+ it.id == id &&
+ (it.sender.equals(event.user.nick, true) || isChannelOp(channel, event))
+ }) {
+ save()
+ event.sendMessage("The message was deleted from the queue.")
+ } else {
+ event.sendMessage("The specified message [ID $id] could not be found.")
+ }
+ }
+ } else {
+ helpResponse(channel, args, event)
+ }
+ }
+
+ override fun isEnabled(): Boolean {
+ return maxSize > 0 && maxDays > 0
+ }
+
+ override fun setProperty(key: String, value: String) {
+ super.setProperty(key, value)
+ if (MAX_DAYS_PROP == key) {
+ maxDays = value.toIntOrDefault(maxDays)
+ } else if (MAX_SIZE_PROP == key) {
+ maxSize = value.toIntOrDefault(maxSize)
+ }
+ }
+
+ // New message.
+ private fun newMessage(channel: String, args: String, event: GenericMessageEvent) {
+ val split = args.split(" ".toRegex(), 2)
+ if (split.size == 2 && split[1].isNotBlank() && split[1].contains(" ")) {
+ if (messages.size < maxSize) {
+ val message = TellMessage(event.user.nick, split[0], split[1].trim())
+ messages.add(message)
+ save()
+ event.sendMessage("Message [ID ${message.id}] was queued for ${message.recipient.bold()}")
+ } else {
+ event.sendMessage("Sorry, the messages queue is currently full.")
+ }
+ } else {
+ helpResponse(channel, args, event)
+ }
+ }
+
+ /**
+ * Saves the messages queue.
+ */
+ private fun save() {
+ TellMessagesMgr.save(serialObject, messages)
+ }
+
+ /**
+ * Checks and sends messages.
+ */
+ fun send(event: GenericUserEvent) {
+ val nickname = event.user.nick
+ if (isEnabled() && nickname != event.getBot().nick) {
+ messages.filter { it.isMatch(nickname) }.forEach { message ->
+ if (message.recipient.equals(nickname, ignoreCase = true) && !message.isReceived) {
+ if (message.sender == nickname) {
+ if (event !is MessageEvent) {
+ event.user.send().message(
+ "${"You".bold()} wanted me to remind you: ${message.message.reverseColor()}"
+ )
+ message.isReceived = true
+ message.isNotified = true
+ save()
+ }
+ } else {
+ event.user.send().message(
+ "${message.sender} wanted me to tell you: ${message.message.reverseColor()}"
+ )
+ message.isReceived = true
+ save()
+ }
+ } else if (message.sender.equals(nickname, ignoreCase = true) && message.isReceived
+ && !message.isNotified
+ ) {
+ event.user.send().message(
+ "Your message ${"[ID ${message.id}]".reverseColor()} was sent to "
+ + "${message.recipient.bold()} on ${message.receptionDate}"
+ )
+ message.isNotified = true
+ save()
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns the messages queue size.
+ *
+ * @return The size.
+ */
+ fun size(): Int = messages.size
+
+ // View all messages.
+ private fun viewAll(event: GenericMessageEvent) {
+ if (messages.isNotEmpty()) {
+ for (message in messages) {
+ event.sendMessage(
+ "${message.sender.bold()}$ARROW${message.recipient.bold()} [ID: ${message.id}, " +
+ (if (message.isReceived) "DELIVERED]" else "QUEUED]")
+ )
+ }
+ } else {
+ event.sendMessage("There are no messages in the queue.")
+ }
+ }
+
+ // View messages.
+ private fun viewMessages(event: GenericMessageEvent) {
+ var hasMessage = false
+ for (message in messages.filter { it.isMatch(event.user.nick) }) {
+ if (!hasMessage) {
+ hasMessage = true
+ event.sendMessage("Here are your messages: ")
+ }
+ if (message.isReceived) {
+ event.sendMessage(
+ message.sender.bold() + ARROW + message.recipient.bold() +
+ " [${message.receptionDate.toUtcDateTime()}, ID: ${message.id.bold()}, DELIVERED]"
+ )
+ } else {
+ event.sendMessage(
+ message.sender.bold() + ARROW + message.recipient.bold() +
+ " [${message.queued.toUtcDateTime()}, ID: ${message.id.bold()}, QUEUED]"
+ )
+ }
+ event.sendMessage(helpFormat(message.message))
+ }
+ if (!hasMessage) {
+ event.sendMessage("You have no messages in the queue.")
+ } else {
+ event.sendMessage("To delete one or all delivered messages:")
+ event.sendMessage(
+ helpFormat(
+ buildCmdSyntax(
+ "%c $name $TELL_DEL_KEYWORD ",
+ event.bot().nick,
+ true
+ )
+ )
+ )
+ event.sendMessage(help.last())
+ }
+ }
+
+ companion object {
+ /**
+ * Max days property.
+ */
+ const val MAX_DAYS_PROP = "tell-max-days"
+
+ /**
+ * Max size property.
+ */
+ const val MAX_SIZE_PROP = "tell-max-size"
+
+ // Arrow
+ private const val ARROW = " --> "
+
+ // All keyword
+ private const val TELL_ALL_KEYWORD = "all"
+
+ //T he delete command.
+ private const val TELL_DEL_KEYWORD = "del"
+ }
+
+ /**
+ * Creates a new instance.
+ */
+ init {
+ initProperties(MAX_DAYS_PROP, MAX_SIZE_PROP)
+
+ // Load the message queue
+ messages.addAll(TellMessagesMgr.load(serialObject))
+ if (clean()) {
+ save()
+ }
+ }
+}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/commands/tell/TellMessage.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/tell/TellMessage.kt
new file mode 100644
index 0000000..a504c92
--- /dev/null
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/tell/TellMessage.kt
@@ -0,0 +1,104 @@
+/*
+ * TellMessage.kt
+ *
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package net.thauvin.erik.mobibot.commands.tell
+
+import java.io.Serializable
+import java.time.Clock
+import java.time.LocalDateTime
+import java.time.format.DateTimeFormatter
+
+/**
+ * The `TellMessage` class.
+ */
+class TellMessage(
+ /**
+ * Returns the message's sender.
+ */
+ val sender: String,
+
+ /**
+ * Returns the message's recipient.
+ */
+ val recipient: String,
+
+ /**
+ * Returns the message text.
+ */
+ val message: String
+) : Serializable {
+ /**
+ * Returns the queued date/time.
+ */
+ var queued: LocalDateTime = LocalDateTime.now(Clock.systemUTC())
+
+ /**
+ * Returns the message id.
+ */
+ var id: String = queued.format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"))
+
+ /**
+ * Returns {@code true} if a notification was sent.
+ */
+ var isNotified = false
+
+ /**
+ * Returns {@code true} if the message was received.
+ */
+ var isReceived = false
+ set(value) {
+ if (value) {
+ receptionDate = LocalDateTime.now(Clock.systemUTC())
+ }
+ field = value
+ }
+
+ /**
+ * Returns the message creating date.
+ */
+ var receptionDate: LocalDateTime = LocalDateTime.MIN
+
+ /**
+ * Matches the message sender or recipient.
+ */
+ fun isMatch(nick: String?): Boolean {
+ return sender.equals(nick, ignoreCase = true) || recipient.equals(nick, ignoreCase = true)
+ }
+
+ override fun toString(): String {
+ return ("TellMessage{id='$id', isNotified=$isNotified, isReceived=$isReceived, message='$message', " +
+ "queued=$queued, received=$receptionDate, recipient='$recipient', sender='$sender'}")
+ }
+
+ companion object {
+ private const val serialVersionUID = 2L
+ }
+}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/commands/tell/TellMessagesMgr.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/tell/TellMessagesMgr.kt
new file mode 100644
index 0000000..bb2f8cd
--- /dev/null
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/tell/TellMessagesMgr.kt
@@ -0,0 +1,103 @@
+/*
+ * TellMessagesMgr.kt
+ *
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package net.thauvin.erik.mobibot.commands.tell
+
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+import java.io.BufferedInputStream
+import java.io.BufferedOutputStream
+import java.io.IOException
+import java.io.ObjectInputStream
+import java.io.ObjectOutputStream
+import java.nio.file.Files
+import java.nio.file.Paths
+import java.time.Clock
+import java.time.LocalDateTime
+import kotlin.io.path.exists
+
+/**
+ * The Tell Messages Manager.
+ */
+object TellMessagesMgr {
+ private val logger: Logger = LoggerFactory.getLogger(TellMessagesMgr::class.java)
+
+ /**
+ * Cleans the messages queue.
+ */
+ @JvmStatic
+ fun clean(tellMessages: MutableList, tellMaxDays: Long): Boolean {
+ if (logger.isDebugEnabled) logger.debug("Cleaning the messages.")
+ val today = LocalDateTime.now(Clock.systemUTC())
+ return tellMessages.removeIf { o: TellMessage -> o.queued.plusDays(tellMaxDays).isBefore(today) }
+ }
+
+ /**
+ * Loads the messages.
+ */
+ @JvmStatic
+ fun load(file: String): List {
+ 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
+ }
+ } 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()
+ }
+
+ /**
+ * Saves the messages.
+ */
+ @JvmStatic
+ fun save(file: String, messages: List?) {
+ try {
+ BufferedOutputStream(Files.newOutputStream(Paths.get(file))).use { bos ->
+ ObjectOutputStream(bos).use { output ->
+ if (logger.isDebugEnabled) logger.debug("Saving the messages.")
+ output.writeObject(messages)
+ }
+ }
+ } catch (e: IOException) {
+ logger.error("Unable to save messages queue.", e)
+ }
+ }
+}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/entries/Entries.kt b/src/main/kotlin/net/thauvin/erik/mobibot/entries/Entries.kt
new file mode 100644
index 0000000..a775168
--- /dev/null
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/entries/Entries.kt
@@ -0,0 +1,55 @@
+/*
+ * Entries.kt
+ *
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package net.thauvin.erik.mobibot.entries
+
+import net.thauvin.erik.mobibot.Utils.today
+
+class Entries(
+ var channel: String = "",
+ var ircServer: String = "",
+ var logsDir: String = "",
+ var backlogs: String = ""
+) {
+ val links = mutableListOf()
+
+ var lastPubDate = today()
+
+ fun load() {
+ lastPubDate = FeedsMgr.loadFeed(this)
+ }
+
+ fun save() {
+ lastPubDate = today()
+ FeedsMgr.saveFeed(this)
+ }
+}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/entries/EntriesUtils.kt b/src/main/kotlin/net/thauvin/erik/mobibot/entries/EntriesUtils.kt
new file mode 100644
index 0000000..023ec86
--- /dev/null
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/entries/EntriesUtils.kt
@@ -0,0 +1,84 @@
+/*
+ * EntriesUtils.kt
+ *
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package net.thauvin.erik.mobibot.entries
+
+import net.thauvin.erik.mobibot.Constants
+import net.thauvin.erik.mobibot.Utils.bold
+import net.thauvin.erik.mobibot.Utils.green
+
+/**
+ * Entries utilities.
+ */
+object EntriesUtils {
+ /**
+ * Build link label based on its index. e.g: L1
+ */
+ @JvmStatic
+ fun buildLinkLabel(index: Int): String = Constants.LINK_CMD + (index + 1)
+
+ /**
+ * Builds an entry's comment for display on the channel.
+ */
+ @JvmStatic
+ fun buildComment(entryIndex: Int, commentIndex: Int, comment: EntryComment): String =
+ ("${buildLinkLabel(entryIndex)}.${commentIndex + 1}: [${comment.nick}] ${comment.comment}")
+
+ /**
+ * Builds an entry's link for display on the channel.
+ */
+ @JvmStatic
+ @JvmOverloads
+ fun buildLink(entryIndex: Int, entry: EntryLink, isView: Boolean = false): String {
+ val buff = StringBuilder().append(buildLinkLabel(entryIndex)).append(": ")
+ .append('[').append(entry.nick).append(']')
+ if (isView && entry.comments.isNotEmpty()) {
+ buff.append("[+").append(entry.comments.size).append(']')
+ }
+ buff.append(' ')
+ with(entry) {
+ if (Constants.NO_TITLE == title) {
+ buff.append(title)
+ } else {
+ buff.append(title.bold())
+ }
+ buff.append(" ( ").append(link.green()).append(" )")
+ }
+ return buff.toString()
+ }
+
+ /**
+ * Build an entry's tags/categories for display on the channel.
+ */
+ @JvmStatic
+ fun buildTags(entryIndex: Int, entry: EntryLink): String =
+ buildLinkLabel(entryIndex) + "${Constants.TAG_CMD}: " + entry.pinboardTags.replace(",", ", ")
+}
diff --git a/src/main/java/net/thauvin/erik/mobibot/msg/PublicMessage.java b/src/main/kotlin/net/thauvin/erik/mobibot/entries/EntryComment.kt
similarity index 67%
rename from src/main/java/net/thauvin/erik/mobibot/msg/PublicMessage.java
rename to src/main/kotlin/net/thauvin/erik/mobibot/entries/EntryComment.kt
index 3c4e924..b0b138a 100644
--- a/src/main/java/net/thauvin/erik/mobibot/msg/PublicMessage.java
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/entries/EntryComment.kt
@@ -1,7 +1,7 @@
/*
- * PublicMessage.java
+ * EntryComment.kt
*
- * Copyright (c) 2004-2019, Erik C. Thauvin (erik@thauvin.net)
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -29,32 +29,24 @@
* 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
-package net.thauvin.erik.mobibot.msg;
+import java.io.Serializable
+import java.time.LocalDateTime
/**
- * The PublicMessage class.
- *
- * @author Erik C. Thauvin
- * @created 2019-04-07
- * @since 1.0
+ * Entry comments data class.
*/
-public class PublicMessage extends Message {
- public PublicMessage(final String message) {
- super();
- this.setMessage(message);
- }
-
+data class EntryComment(var comment: String, var nick: String) : Serializable {
/**
- * Creates a new public message.
- *
- * @param message The message.
- * @param color The message color.
+ * Creation date.
*/
- @SuppressWarnings("unused")
- public PublicMessage(final String message, final String color) {
- super();
- this.setMessage(message);
- this.setColor(color);
+ val date: LocalDateTime = LocalDateTime.now()
+
+ override fun toString(): String = "EntryComment{comment='$comment', date=$date, nick='$nick'}"
+
+ companion object {
+ // Serial version UID
+ const val serialVersionUID = 1L
}
}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/entries/EntryLink.kt b/src/main/kotlin/net/thauvin/erik/mobibot/entries/EntryLink.kt
new file mode 100644
index 0000000..9177ebe
--- /dev/null
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/entries/EntryLink.kt
@@ -0,0 +1,220 @@
+/*
+ * EntryLink.kt
+ *
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package net.thauvin.erik.mobibot.entries
+
+import com.rometools.rome.feed.synd.SyndCategory
+import com.rometools.rome.feed.synd.SyndCategoryImpl
+import net.thauvin.erik.mobibot.commands.links.LinksMgr
+import java.io.Serializable
+import java.util.Calendar
+import java.util.Date
+
+/**
+ * The class used to store link entries.
+ */
+class EntryLink(
+ // Link's comments
+ val comments: MutableList = mutableListOf(),
+
+ // Tags/categories
+ val tags: MutableList = mutableListOf(),
+
+ // Channel
+ var channel: String,
+
+ // Creation date
+ var date: Date = Calendar.getInstance().time,
+
+ // Link's URL
+ var link: String,
+
+ // Author's login
+ var login: String = "",
+
+ // Author's nickname
+ var nick: String,
+
+ // Link's title
+ var title: String
+) : Serializable {
+ /**
+ * Creates a new entry.
+ */
+ constructor(
+ link: String,
+ title: String,
+ nick: String,
+ login: String,
+ channel: String,
+ tags: List
+ ) : this(link = link, title = title, nick = nick, login = login, channel = channel) {
+ setTags(tags)
+ }
+
+ /**
+ * Creates a new entry.
+ */
+ constructor(
+ link: String,
+ title: String,
+ nick: String,
+ channel: String,
+ date: Date,
+ tags: List
+ ) : this(link = link, title = title, nick = nick, channel = channel, date = Date(date.time)) {
+ this.tags.addAll(tags)
+ }
+
+ /**
+ * Adds a new comment
+ */
+ fun addComment(comment: EntryComment): Int {
+ comments.add(comment)
+ return comments.lastIndex
+ }
+
+ /**
+ * Adds a new comment.
+ */
+ fun addComment(comment: String, nick: String): Int {
+ return addComment(EntryComment(comment, nick))
+ }
+
+ /**
+ * Deletes a specific comment.
+ */
+ fun deleteComment(index: Int): Boolean {
+ if (index < comments.size) {
+ comments.removeAt(index)
+ return true
+ }
+ return false
+ }
+
+ /**
+ * Deletes a comment.
+ */
+ fun deleteComment(entryComment: EntryComment): Boolean {
+ return comments.remove(entryComment)
+ }
+
+ /**
+ * Returns a comment.
+ */
+ fun getComment(index: Int): EntryComment = comments[index]
+
+ /**
+ * Returns the tags formatted for pinboard.in
+ */
+ val pinboardTags: String
+ get() {
+ val pinboardTags = StringBuilder(nick)
+ for (tag in tags) {
+ pinboardTags.append(',')
+ pinboardTags.append(tag.name)
+ }
+ return pinboardTags.toString()
+ }
+
+ /**
+ * Returns true if a string is contained in the link, title, or nick.
+ */
+ fun matches(match: String?): Boolean {
+ return if (match.isNullOrEmpty()) {
+ false
+ } else {
+ link.contains(match, true) || title.contains(match, true) || nick.contains(match, true)
+ }
+ }
+
+ /**
+ * Sets a comment.
+ */
+ fun setComment(index: Int, comment: String?, nick: String?) {
+ if (index < comments.size && !comment.isNullOrBlank() && !nick.isNullOrBlank()) {
+ comments[index] = EntryComment(comment, nick)
+ }
+ }
+
+ /**
+ * Sets the tags.
+ */
+ fun setTags(tags: String) {
+ setTags(tags.split(LinksMgr.TAG_MATCH))
+ }
+
+ /**
+ * Sets the tags.
+ */
+ private fun setTags(tags: List) {
+ if (tags.isNotEmpty()) {
+ var category: SyndCategoryImpl
+ for (tag in tags) {
+ if (!tag.isNullOrBlank()) {
+ val t = tag.lowercase()
+ val mod = t[0]
+ if (mod == '-') {
+ // Don't remove the channel tag
+ if (channel.substring(1) != t.substring(1)) {
+ category = SyndCategoryImpl()
+ category.name = t.substring(1)
+ this.tags.remove(category)
+ }
+ } else {
+ category = SyndCategoryImpl()
+ if (mod == '+') {
+ category.name = t.substring(1)
+ } else {
+ category.name = t
+ }
+ if (!this.tags.contains(category)) {
+ this.tags.add(category)
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns a string representation of the object.
+ */
+ override fun toString(): String {
+ return ("EntryLink{channel='$channel', comments=$comments, date=$date, link='$link', login='$login'," +
+ "nick='$nick', tags=$tags, title='$title'}")
+ }
+
+ companion object {
+ // Serial version UID
+ const val serialVersionUID = 1L
+ }
+}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/entries/FeedsMgr.kt b/src/main/kotlin/net/thauvin/erik/mobibot/entries/FeedsMgr.kt
new file mode 100644
index 0000000..9060146
--- /dev/null
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/entries/FeedsMgr.kt
@@ -0,0 +1,192 @@
+/*
+ * FeedsMgr.kt
+ *
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package net.thauvin.erik.mobibot.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.
+ */
+ @JvmStatic
+ @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
+ for (comment in description.value.split(" ")) {
+ split = comment.split(": ".toRegex(), 2)
+ if (split.size == 2) {
+ entry.addComment(comment = split[1].trim(), nick = split[0].trim())
+ }
+ }
+ }
+ entries.links.add(entry)
+ }
+ }
+ } else {
+ // Create an empty feed.
+ saveFeed(entries)
+ }
+ return pubDate
+ }
+
+ /**
+ * Saves the feeds.
+ */
+ @JvmStatic
+ fun saveFeed(entries: Entries, currentFile: String = currentXml) {
+ if (logger.isDebugEnabled) logger.debug("Saving the feeds...")
+ if (entries.logsDir.isNotBlank()) {
+ try {
+ val output = SyndFeedOutput()
+ val rss: SyndFeed = SyndFeedImpl()
+ val items: MutableList = mutableListOf()
+ var item: SyndEntry
+ OutputStreamWriter(
+ Files.newOutputStream(Paths.get("${entries.logsDir}${currentFile}")), StandardCharsets.UTF_8
+ ).use { fw ->
+ with(rss) {
+ feedType = "rss_2.0"
+ title = "${entries.channel} IRC Links"
+ description = "Links from ${entries.ircServer} on ${entries.channel}"
+ if (entries.backlogs.isNotBlank()) link = entries.backlogs
+ publishedDate = Calendar.getInstance().time
+ language = "en"
+ }
+ val buff: StringBuilder = StringBuilder()
+ for (i in entries.links.indices.reversed()) {
+ with(entries.links[i]) {
+ buff.setLength(0)
+ buff.append("Posted by ")
+ .append(nick)
+ .append(" on ")
+ .append(channel)
+ .append("")
+ if (comments.size > 0) {
+ buff.append("
")
+ for (j in comments.indices) {
+ if (j > 0) {
+ buff.append(" ")
+ }
+ 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.")
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/modules/AbstractModule.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/AbstractModule.kt
new file mode 100644
index 0000000..25e7c9d
--- /dev/null
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/AbstractModule.kt
@@ -0,0 +1,132 @@
+/*
+ * AbstractModule.kt
+ *
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package net.thauvin.erik.mobibot.modules
+
+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 {
+ /**
+ * The module name.
+ */
+ abstract val name: String
+
+ /**
+ * The module's commands, if any.
+ */
+ @JvmField
+ val commands: MutableList = mutableListOf()
+
+ @JvmField
+ val help: MutableList = mutableListOf()
+ val properties: MutableMap = mutableMapOf()
+
+ /**
+ * Responds to a command.
+ */
+ abstract fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent)
+
+ /**
+ * Returns the module's property keys.
+ */
+ val propertyKeys: Set
+ get() = properties.keys
+
+ /**
+ * Returns `true` if the module has properties.
+ */
+ fun hasProperties(): Boolean {
+ return properties.isNotEmpty()
+ }
+
+ /**
+ * Responds with the module's help.
+ */
+ open fun helpResponse(event: GenericMessageEvent): Boolean {
+ for (h in help) {
+ event.sendMessage(buildCmdSyntax(h, event.bot().nick, isPrivateMsgEnabled && event is PrivateMessageEvent))
+ }
+ return true
+ }
+
+ /**
+ * Initializes the properties.
+ */
+ fun initProperties(vararg keys: String) {
+ for (key in keys) {
+ properties[key] = ""
+ }
+ }
+
+ /**
+ * Returns `true` if the module is enabled.
+ */
+ val isEnabled: Boolean
+ get() = if (hasProperties()) {
+ isValidProperties
+ } else {
+ true
+ }
+
+ /**
+ * Returns `true` if the module responds to private messages.
+ */
+ open val isPrivateMsgEnabled: Boolean = false
+
+ /**
+ * Ensures that all properties have values.
+ */
+ open val isValidProperties: Boolean
+ get() {
+ for (s in propertyKeys) {
+ if (properties[s].isNullOrBlank()) {
+ return false
+ }
+ }
+ return true
+ }
+
+ /**
+ * Sets a property key and value.
+ */
+ fun setProperty(key: String, value: String) {
+ if (key.isNotBlank()) {
+ properties[key] = value
+ }
+ }
+}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/modules/Calc.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/Calc.kt
new file mode 100644
index 0000000..76f3786
--- /dev/null
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/Calc.kt
@@ -0,0 +1,88 @@
+/*
+ * Calc.kt
+ *
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package net.thauvin.erik.mobibot.modules
+
+import net.objecthunter.exp4j.ExpressionBuilder
+import net.objecthunter.exp4j.tokenizer.UnknownFunctionOrVariableException
+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 : AbstractModule() {
+ private val logger: Logger = LoggerFactory.getLogger(Calc::class.java)
+
+ override val name = "Calc"
+
+ override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
+ if (args.isNotBlank()) {
+ 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(event)
+ }
+ }
+
+ companion object {
+ // Calc command
+ private const val CALC_CMD = "calc"
+
+ /**
+ * Performs a calculation. e.g.: 1 + 1 * 2
+ */
+ @JvmStatic
+ @Throws(IllegalArgumentException::class)
+ fun calculate(query: String): String {
+ val decimalFormat = DecimalFormat("#.##")
+ val calc = ExpressionBuilder(query).build()
+ return query.replace(" ", "") + " = " + decimalFormat.format(calc.evaluate()).bold()
+ }
+ }
+
+ init {
+ commands.add(CALC_CMD)
+ help.add("To solve a mathematical calculation:")
+ help.add(helpFormat("%c $CALC_CMD "))
+ }
+}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/modules/CryptoPrices.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/CryptoPrices.kt
new file mode 100644
index 0000000..8cb3396
--- /dev/null
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/CryptoPrices.kt
@@ -0,0 +1,108 @@
+/*
+ * CryptoPrices.kt
+ *
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package net.thauvin.erik.mobibot.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.Utils.helpFormat
+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 : ThreadedModule() {
+ private val logger: Logger = LoggerFactory.getLogger(CryptoPrices::class.java)
+
+ override val name = "CryptoPrices"
+
+ /**
+ * Returns the cryptocurrency market price from [Coinbase](https://developers.coinbase.com/api/v2#get-spot-price).
+ */
+ override fun run(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
+ val debugMessage = "crypto($cmd $args)"
+ 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
+ }
+ event.respond("${price.base} current price is $amount [${price.currency}]")
+ } catch (e: CryptoException) {
+ if (logger.isWarnEnabled) logger.warn("$debugMessage => ${e.statusCode}", e)
+ e.message?.let {
+ event.sendMessage(it)
+ }
+ } 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 {
+ // Crypto command
+ private const val CRYPTO_CMD = "crypto"
+
+ /**
+ * Get current market price.
+ */
+ @JvmStatic
+ fun currentPrice(args: List): CryptoPrice {
+ return if (args.size == 2)
+ spotPrice(args[0], args[1])
+ else
+ spotPrice(args[0])
+ }
+ }
+
+ init {
+ commands.add(CRYPTO_CMD)
+ with(help) {
+ add("To retrieve a cryptocurrency's market price:")
+ add(helpFormat("%c $CRYPTO_CMD []"))
+ add("For example:")
+ add(helpFormat("%c $CRYPTO_CMD BTC"))
+ add(helpFormat("%c $CRYPTO_CMD ETH EUR"))
+ add(helpFormat("%c $CRYPTO_CMD ETH2 GPB"))
+ }
+ }
+}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/modules/CurrencyConverter.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/CurrencyConverter.kt
new file mode 100644
index 0000000..f6de896
--- /dev/null
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/CurrencyConverter.kt
@@ -0,0 +1,234 @@
+/*
+ * CurrencyConverter.kt
+ *
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package net.thauvin.erik.mobibot.modules
+
+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
+import java.util.Currency
+import java.util.Locale
+import javax.xml.XMLConstants
+
+/**
+ * The CurrencyConverter module.
+ */
+class CurrencyConverter : ThreadedModule() {
+ private val logger: Logger = LoggerFactory.getLogger(CurrencyConverter::class.java)
+
+ override val name = "CurrencyConverter"
+
+ override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
+ synchronized(this) {
+ if (pubDate != today()) {
+ EXCHANGE_RATES.clear()
+ }
+ }
+ super.commandResponse(channel, cmd, args, event)
+ }
+
+ /**
+ * Converts the specified currencies.
+ */
+ 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()) {
+ 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 ${pubDate.bold()} are:")
+ event.sendList(currencyRates(), 3, " ", isIndent = true)
+ } else {
+ helpResponse(event)
+ }
+ }
+
+ 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()) {
+ 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)
+ )
+ )
+ event.sendMessage("The supported currencies are: ")
+ event.sendList(ArrayList(EXCHANGE_RATES.keys), 11, isIndent = true)
+ }
+ return true
+ }
+
+ companion object {
+ // Currency command
+ private const val CURRENCY_CMD = "currency"
+
+ // Rates keyword
+ private const val CURRENCY_RATES_KEYWORD = "rates"
+
+ // Empty rate table.
+ private const val EMPTY_RATE_TABLE = "Sorry, but the exchange rate table is empty."
+
+ // Exchange rates
+ private val EXCHANGE_RATES: MutableMap = mutableMapOf()
+
+ // Exchange rates table URL
+ private const val EXCHANGE_TABLE_URL = "https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml"
+
+ // Last exchange rates table publication date
+ private var pubDate = ""
+
+ private fun Double.formatCurrency(currency: String): String =
+ NumberFormat.getCurrencyInstance(Locale.getDefault(Locale.Category.FORMAT)).let {
+ it.currency = Currency.getInstance(currency)
+ it.format(this)
+ }
+
+ /**
+ * Converts from a currency to another.
+ */
+ @JvmStatic
+ fun convertCurrency(query: String): Message {
+ val cmds = query.split(" ")
+ return if (cmds.size == 4) {
+ if (cmds[3] == cmds[1] || "0" == cmds[0]) {
+ PublicMessage("You're kidding, right?")
+ } else {
+ val to = cmds[1].uppercase()
+ val from = cmds[3].uppercase()
+ if (EXCHANGE_RATES.containsKey(to) && EXCHANGE_RATES.containsKey(from)) {
+ try {
+ val amt = cmds[0].replace(",", "").toDouble()
+ val doubleFrom = EXCHANGE_RATES[to]!!.toDouble()
+ val doubleTo = EXCHANGE_RATES[from]!!.toDouble()
+ PublicMessage(
+ amt.formatCurrency(to) + " = " + (amt * doubleTo / doubleFrom).formatCurrency(from)
+ )
+ } catch (e: NumberFormatException) {
+ ErrorMessage("Let's try with some real numbers next time, okay?")
+ }
+ } else {
+ ErrorMessage("Sounds like monopoly money to me!")
+ }
+ }
+ } else ErrorMessage("Invalid query. Let's try again.")
+ }
+
+
+ @JvmStatic
+ fun currencyRates(): List {
+ val rates = buildList {
+ for ((key, value) in EXCHANGE_RATES.toSortedMap()) {
+ add("$key: ${value.padStart(8)}")
+ }
+ }
+ return rates
+ }
+
+ @JvmStatic
+ @Throws(ModuleException::class)
+ fun loadRates() {
+ if (EXCHANGE_RATES.isEmpty()) {
+ try {
+ val builder = SAXBuilder()
+ // See https://rules.sonarsourcecom/java/tag/owasp/RSPEC-2755
+ builder.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "")
+ builder.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "")
+ builder.ignoringElementContentWhitespace = true
+ val doc = builder.build(URL(EXCHANGE_TABLE_URL))
+ val root = doc.rootElement
+ val ns = root.getNamespace("")
+ val cubeRoot = root.getChild("Cube", ns)
+ val cubeTime = cubeRoot.getChild("Cube", ns)
+ pubDate = cubeTime.getAttribute("time").value
+ val cubes = cubeTime.children
+ for (cube in cubes) {
+ EXCHANGE_RATES[cube.getAttribute("currency").value] = cube.getAttribute("rate").value
+ }
+ EXCHANGE_RATES["EUR"] = "1"
+ } catch (e: JDOMException) {
+ throw ModuleException(
+ "loadRates(): JDOM",
+ "An JDOM parsing error has occurred while parsing the exchange rates table.",
+ e
+ )
+ } catch (e: IOException) {
+ throw ModuleException(
+ "loadRates(): IOE",
+ "An IO error has occurred while parsing the exchange rates table.",
+ e
+ )
+ }
+ }
+ }
+ }
+
+ init {
+ commands.add(CURRENCY_CMD)
+ }
+}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/modules/Dice.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/Dice.kt
new file mode 100644
index 0000000..9e7725d
--- /dev/null
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/Dice.kt
@@ -0,0 +1,102 @@
+/*
+ * Dice.kt
+ *
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package net.thauvin.erik.mobibot.modules
+
+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
+
+/**
+ * The Dice module.
+ */
+class Dice : AbstractModule() {
+ override val name = "Dice"
+
+ 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(event.bot()) {
+ event.respond(
+ "you rolled ${DICE_FACES[roll.first]} ${DICE_FACES[roll.second]} for a total of ${total.bold()}"
+ )
+ sendIRC().action(
+ channel,
+ "rolled ${DICE_FACES[botRoll.first]} ${DICE_FACES[botRoll.second]} for a total of ${botTotal.bold()}"
+ )
+ when (winLoseOrTie(botTotal, total)) {
+ Result.WIN -> sendIRC().action(channel, "wins.")
+ Result.LOSE -> sendIRC().action(channel, "lost.")
+ else -> sendIRC().action(channel, "tied.")
+ }
+ }
+ }
+
+ enum class Result {
+ WIN, LOSE, TIE
+ }
+
+ private fun roll(): Pair {
+ return (1..DICE_FACES.size).random() to (1..DICE_FACES.size).random()
+ }
+
+ companion object {
+ // Dice command
+ private const val DICE_CMD = "dice"
+
+ // Dice faces
+ private val DICE_FACES = arrayOf("", "⚀", "⚁", "⚂", "⚃", "⚄", "⚅")
+
+ @JvmStatic
+ fun winLoseOrTie(bot: Int, player: Int): Result {
+ return when {
+ bot > player -> {
+ Result.WIN
+ }
+ bot < player -> {
+ Result.LOSE
+ }
+ else -> {
+ Result.TIE
+ }
+ }
+ }
+ }
+
+ init {
+ commands.add(DICE_CMD)
+ help.add("To roll the dice:")
+ help.add(helpFormat("%c $DICE_CMD"))
+ }
+}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/modules/GoogleSearch.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/GoogleSearch.kt
new file mode 100644
index 0000000..6adffdd
--- /dev/null
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/GoogleSearch.kt
@@ -0,0 +1,146 @@
+/*
+ * GoogleSearch.kt
+ *
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package net.thauvin.erik.mobibot.modules
+
+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.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 : ThreadedModule() {
+ private val logger: Logger = LoggerFactory.getLogger(GoogleSearch::class.java)
+
+ override val name = "GoogleSearch"
+
+ /**
+ * Searches Google.
+ */
+ 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)
+ }
+ } catch (e: ModuleException) {
+ if (logger.isWarnEnabled) logger.warn(e.debugMessage, e)
+ e.message?.let {
+ event.sendMessage(it)
+ }
+ }
+ } else {
+ helpResponse(event)
+ }
+ }
+
+ companion object {
+ // Google API Key property
+ const val GOOGLE_API_KEY_PROP = "google-api-key"
+
+ // Google Custom Search Engine ID property
+ const val GOOGLE_CSE_KEY_PROP = "google-cse-cx"
+
+ // Google command
+ private const val GOOGLE_CMD = "google"
+
+ /**
+ * Performs a search on Google.
+ */
+ @JvmStatic
+ @Throws(ModuleException::class)
+ fun searchGoogle(query: String, apiKey: String?, cseKey: String?): List {
+ if (apiKey.isNullOrBlank() || cseKey.isNullOrBlank()) {
+ throw ModuleException(
+ "${GoogleSearch::class.java.name} is disabled.",
+ "${GOOGLE_CMD.capitalise()} is disabled. The API keys are missing."
+ )
+ }
+ val results = mutableListOf()
+ 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))
+ 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): IOE", "An IO error has occurred searching Google.", e)
+ } catch (e: JSONException) {
+ throw ModuleException(
+ "searchGoogle($query): JSON",
+ "A JSON error has occurred searching Google.",
+ e
+ )
+ }
+ } else {
+ results.add(ErrorMessage("Invalid query. Please try again."))
+ }
+ return results
+ }
+ }
+
+ init {
+ commands.add(GOOGLE_CMD)
+ help.add("To search Google:")
+ help.add(helpFormat("%c $GOOGLE_CMD "))
+ initProperties(GOOGLE_API_KEY_PROP, GOOGLE_CSE_KEY_PROP)
+ }
+}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/modules/Joke.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/Joke.kt
new file mode 100644
index 0000000..6922698
--- /dev/null
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/Joke.kt
@@ -0,0 +1,115 @@
+/*
+ * Joke.kt
+ *
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package net.thauvin.erik.mobibot.modules
+
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+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 : ThreadedModule() {
+ private val logger: Logger = LoggerFactory.getLogger(Joke::class.java)
+
+ override val name = "Joke"
+
+ override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
+ runBlocking {
+ launch { run(channel, cmd, args, event) }
+ }
+ }
+
+ /**
+ * Returns a random joke from [The Internet Chuck Norris Database](http://www.icndb.com/).
+ */
+ override fun run(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
+ with(event.bot()) {
+ try {
+ sendIRC().notice(channel, randomJoke().msg.cyan())
+ } catch (e: ModuleException) {
+ if (logger.isWarnEnabled) logger.warn(e.debugMessage, e)
+ e.message?.let {
+ event.sendMessage(it)
+ }
+ }
+ }
+ }
+
+ companion object {
+ // Joke command
+ private const val JOKE_CMD = "joke"
+
+ // ICNDB URL
+ private const val JOKE_URL =
+ "http://api.icndb.com/jokes/random?escape=javascript&exclude=[explicit]&limitTo=[nerdy]"
+
+ /**
+ * Retrieves a random joke.
+ */
+ @JvmStatic
+ @Throws(ModuleException::class)
+ fun randomJoke(): Message {
+ return try {
+ val url = URL(JOKE_URL)
+ val json = JSONObject(urlReader(url))
+ PublicMessage(
+ json.getJSONObject("value")["joke"].toString().replace("\\'", "'")
+ .replace("\\\"", "\"")
+ )
+ } catch (e: IOException) {
+ throw ModuleException("randomJoke(): IOE", "An IO error has occurred retrieving a random joke.", e)
+ } catch (e: JSONException) {
+ throw ModuleException("randomJoke(): JSON", "An JSON error has occurred retrieving a random joke.", e)
+ }
+ }
+ }
+
+ init {
+ commands.add(JOKE_CMD)
+ help.add("To retrieve a random joke:")
+ help.add(helpFormat("%c $JOKE_CMD"))
+ }
+}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/modules/Lookup.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/Lookup.kt
new file mode 100644
index 0000000..90a0316
--- /dev/null
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/Lookup.kt
@@ -0,0 +1,172 @@
+/*
+ * Lookup.kt
+ *
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package net.thauvin.erik.mobibot.modules
+
+import net.thauvin.erik.mobibot.Constants
+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
+
+/**
+ * The Lookup module.
+ */
+class Lookup : AbstractModule() {
+ private val logger: Logger = LoggerFactory.getLogger(Lookup::class.java)
+
+ override val name = "Lookup"
+
+ override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
+ if (args.matches("(\\S.)+(\\S)+".toRegex())) {
+ 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]?)\\.){3}(?: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 {
+ event.respond("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(event)
+ }
+ }
+
+ companion object {
+ /**
+ * The whois default host.
+ */
+ const val WHOIS_HOST = "whois.arin.net"
+
+ // Lookup command
+ private const val LOOKUP_CMD = "lookup"
+
+ /**
+ * Performs a DNS lookup on the specified query.
+ */
+ @JvmStatic
+ @Throws(UnknownHostException::class)
+ fun nslookup(query: String): String {
+ val buffer = StringBuilder()
+ val results = InetAddress.getAllByName(query)
+ var hostInfo: String
+ for (result in results) {
+ if (result.hostAddress == query) {
+ hostInfo = result.hostName
+ if (hostInfo == query) {
+ throw UnknownHostException()
+ }
+ } else {
+ hostInfo = result.hostAddress
+ }
+ if (buffer.isNotEmpty()) {
+ buffer.append(", ")
+ }
+ buffer.append(hostInfo)
+ }
+ return buffer.toString()
+ }
+
+ /**
+ * Performs a whois IP query.
+ */
+ @Throws(IOException::class)
+ private fun whois(query: String): List {
+ return whois(query, WHOIS_HOST)
+ }
+
+ /**
+ * Performs a whois IP query.
+ */
+ @JvmStatic
+ @Throws(IOException::class)
+ fun whois(query: String, host: String): List {
+ val whoisClient = WhoisClient()
+ val lines: List
+ with(whoisClient) {
+ try {
+ defaultTimeout = Constants.CONNECT_TIMEOUT
+ connect(host)
+ soTimeout = Constants.CONNECT_TIMEOUT
+ setSoLinger(false, 0)
+ lines = if (WHOIS_HOST == host) {
+ query("n - $query").split("\n")
+ } else {
+ query(query).split("\n")
+ }
+ } finally {
+ disconnect()
+ }
+ }
+ return lines
+ }
+ }
+
+ init {
+ commands.add(LOOKUP_CMD)
+ help.add("To perform a DNS lookup query:")
+ help.add(helpFormat("%c $LOOKUP_CMD "))
+ }
+}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/modules/ModuleException.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/ModuleException.kt
new file mode 100644
index 0000000..2f39854
--- /dev/null
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/ModuleException.kt
@@ -0,0 +1,45 @@
+/*
+ * ModuleException.kt
+ *
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package net.thauvin.erik.mobibot.modules
+
+/**
+ * The `ModuleException` class.
+ */
+class ModuleException @JvmOverloads constructor(
+ val debugMessage: String,
+ message: String? = null,
+ cause: Throwable? = null
+) : Exception(message, cause) {
+ companion object {
+ private const val serialVersionUID = 1L
+ }
+}
diff --git a/src/test/java/net/thauvin/erik/mobibot/modules/AbstractModuleTest.java b/src/main/kotlin/net/thauvin/erik/mobibot/modules/Ping.kt
similarity index 51%
rename from src/test/java/net/thauvin/erik/mobibot/modules/AbstractModuleTest.java
rename to src/main/kotlin/net/thauvin/erik/mobibot/modules/Ping.kt
index 5eab364..0818120 100644
--- a/src/test/java/net/thauvin/erik/mobibot/modules/AbstractModuleTest.java
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/Ping.kt
@@ -1,7 +1,7 @@
/*
- * AbstractModuleTest.java
+ * Ping.kt
*
- * Copyright (c) 2004-2020, Erik C. Thauvin (erik@thauvin.net)
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -29,41 +29,56 @@
* 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.modules
-package net.thauvin.erik.mobibot.modules;
-
-import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
-
-import static org.assertj.core.api.Assertions.assertThat;
+import net.thauvin.erik.mobibot.Utils.bot
+import net.thauvin.erik.mobibot.Utils.helpFormat
+import org.pircbotx.hooks.types.GenericMessageEvent
/**
- * The AbstractModuleTest class.
- *
- * @author Erik C. Thauvin
- * @created 2019-04-07
- * @since 1.0
+ * The Ping module.
*/
+class Ping : AbstractModule() {
+ override val name = "Ping"
-final class AbstractModuleTest {
- private AbstractModuleTest() {
- throw new UnsupportedOperationException("Illegal constructor call.");
+ override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
+ event.bot().sendIRC().action(channel, randomPing())
}
- @SuppressFBWarnings("CE_CLASS_ENVY")
- static void testAbstractModule(final AbstractModule module) {
- final String name = module.getClass().getName();
+ companion object {
+ /**
+ * The ping responses.
+ */
+ @JvmField
+ val PINGS = listOf(
+ "is barely alive.",
+ "is trying to stay awake.",
+ "has gone fishing.",
+ "is somewhere over the rainbow.",
+ "has fallen and can't get up.",
+ "is running. You better go chase it.",
+ "has just spontaneously combusted.",
+ "is talking to itself... don't interrupt. That's rude.",
+ "is bartending at an AA meeting.",
+ "is hibernating.",
+ "is saving energy: apathetic mode activated.",
+ "is busy. Go away!"
+ )
- assertThat(module.isEnabled()).as(name + ": enabled").isNotEqualTo(module.hasProperties());
- assertThat(module.getCommands().size()).as(name + ": commands > 0").isGreaterThan(0);
- if (!module.hasProperties()) {
- assertThat(module.getPropertyKeys().size()).as(name + ": no properties").isEqualTo(0);
- module.setProperty("test", "test");
- module.setProperty("", "invalid");
+ @JvmStatic
+ fun randomPing(): String {
+ return PINGS[PINGS.indices.random()]
}
- assertThat(module.getPropertyKeys().size()).as(name + ": properties > 0").isGreaterThan(0);
+ /**
+ * The ping command.
+ */
+ private const val PING_CMD = "ping"
+ }
- module.setProperty("invalid", "");
- assertThat(module.isValidProperties()).as(name + ": invalid properties").isFalse();
+ init {
+ commands.add(PING_CMD)
+ help.add("To ping the bot:")
+ help.add(helpFormat("%c $PING_CMD"))
}
}
diff --git a/src/main/java/net/thauvin/erik/mobibot/modules/RockPaperScissors.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/RockPaperScissors.kt
similarity index 55%
rename from src/main/java/net/thauvin/erik/mobibot/modules/RockPaperScissors.kt
rename to src/main/kotlin/net/thauvin/erik/mobibot/modules/RockPaperScissors.kt
index 903bf90..c092548 100644
--- a/src/main/java/net/thauvin/erik/mobibot/modules/RockPaperScissors.kt
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/RockPaperScissors.kt
@@ -1,7 +1,7 @@
/*
* RockPaperScissors.kt
*
- * Copyright (c) 2004-2020, Erik C. Thauvin (erik@thauvin.net)
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -32,20 +32,33 @@
package net.thauvin.erik.mobibot.modules
-import net.thauvin.erik.mobibot.Mobibot
-import net.thauvin.erik.mobibot.Utils
-import kotlin.random.Random
+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
/**
* Simple module example in Kotlin.
*/
class RockPaperScissors : AbstractModule() {
+ override val name = "RockPaperScissors"
+
init {
with(commands) {
- add(Hands.ROCK.name.toLowerCase())
- add(Hands.PAPER.name.toLowerCase())
- add(Hands.SCISSORS.name.toLowerCase())
+ add(Hands.ROCK.name.lowercase())
+ add(Hands.PAPER.name.lowercase())
+ add(Hands.SCISSORS.name.lowercase())
+ }
+
+ with(help) {
+ add("To play Rock Paper Scissors:")
+ add(
+ helpFormat(
+ "%c ${Hands.ROCK.name.lowercase()} | ${Hands.PAPER.name.lowercase()}"
+ + " | ${Hands.SCISSORS.name.lowercase()}"
+ )
+ )
}
}
@@ -54,26 +67,33 @@ class RockPaperScissors : AbstractModule() {
override fun beats(hand: Hands): Boolean {
return hand == SCISSORS
}
+
+ override var emoji = "\u270A"
},
PAPER("covers") {
override fun beats(hand: Hands): Boolean {
return hand == ROCK
}
+
+ override var emoji = "\u270B"
},
SCISSORS("cuts") {
override fun beats(hand: Hands): Boolean {
return hand == PAPER
}
+
+ override var emoji = "\u270C"
};
abstract fun beats(hand: Hands): Boolean
+ abstract var emoji: String
}
companion object {
// For testing.
- fun winLoseOrDraw(player: String, bot: String ): String {
- val hand = Hands.valueOf(player.toUpperCase())
- val botHand = Hands.valueOf(bot.toUpperCase())
+ fun winLoseOrDraw(player: String, bot: String): String {
+ val hand = Hands.valueOf(player.uppercase())
+ val botHand = Hands.valueOf(bot.uppercase())
return when {
hand == botHand -> "draw"
@@ -83,34 +103,28 @@ class RockPaperScissors : AbstractModule() {
}
}
- override fun commandResponse(bot: Mobibot, sender: String, cmd: String, args: String?, isPrivate: Boolean) {
- val hand = Hands.valueOf(cmd.toUpperCase())
- val botHand = Hands.values()[Random.nextInt(0, Hands.values().size)]
- when {
- hand == botHand -> {
- bot.action("${Utils.green(hand.name)} vs. ${Utils.green(botHand.name)} ~ The game is tied ~")
- }
- hand.beats(botHand) -> {
- bot.action(
- "${Utils.green(hand.name)} ${Utils.bold(hand.action)} ${Utils.red(botHand.name)} ~ You win ~"
- )
- }
- else -> {
- bot.action(
- "${Utils.green(botHand.name)} ${Utils.bold(botHand.action)} ${Utils.red(botHand.name)} ~ You lose ~"
- )
+ override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
+ val hand = Hands.valueOf(cmd.uppercase())
+ val botHand = Hands.values()[(0..Hands.values().size).random()]
+ with(event.bot()) {
+ sendIRC().message(channel, "${hand.emoji} vs. ${botHand.emoji}")
+ when {
+ hand == botHand -> {
+ sendIRC().action(channel, "tied.")
+ }
+ hand.beats(botHand) -> {
+ sendIRC().action(
+ channel,
+ "lost. ${hand.name.capitalise()} ${hand.action} ${botHand.name.lowercase()}."
+ )
+ }
+ else -> {
+ sendIRC().action(
+ channel,
+ "wins. ${botHand.name.capitalise()} ${botHand.action} ${hand.name.lowercase()}."
+ )
+ }
}
}
}
-
- override fun helpResponse(bot: Mobibot, sender: String, args: String?, isPrivate: Boolean) {
- bot.send(sender, "To play Rock Paper Scissors:")
- bot.send(
- sender,
- bot.helpIndent(
- "${bot.nick}: ${Hands.ROCK.name.toLowerCase()} or ${Hands.PAPER.name.toLowerCase()}"
- + " or ${Hands.SCISSORS.name.toLowerCase()}"
- )
- )
- }
}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/modules/StockQuote.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/StockQuote.kt
new file mode 100644
index 0000000..63d358c
--- /dev/null
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/StockQuote.kt
@@ -0,0 +1,241 @@
+/*
+ * StockQuote.kt
+ *
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package net.thauvin.erik.mobibot.modules
+
+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 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 : ThreadedModule() {
+ private val logger: Logger = LoggerFactory.getLogger(StockQuote::class.java)
+
+ override val name = "StockQuote"
+
+ /**
+ * Returns the specified stock quote from Alpha Vantage.
+ */
+ 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)
+ }
+ } catch (e: ModuleException) {
+ if (logger.isWarnEnabled) logger.warn(e.debugMessage, e)
+ e.message?.let {
+ event.sendMessage(it)
+ }
+ }
+ } else {
+ helpResponse(event)
+ }
+ }
+
+ companion object {
+ /**
+ * The Alpha Advantage property key.
+ */
+ const val ALPHAVANTAGE_API_KEY_PROP = "alphavantage-api-key"
+
+ /**
+ * The Invalid Symbol error string.
+ */
+ const val INVALID_SYMBOL = "Invalid symbol."
+
+ // Alpha Advantage URL
+ private const val ALPHAVANTAGE_URL = "https://www.alphavantage.co/query?function="
+
+ // Quote command
+ private const val STOCK_CMD = "stock"
+
+ @Throws(ModuleException::class)
+ private fun getJsonResponse(response: String, debugMessage: String): JSONObject {
+ return if (response.isNotBlank()) {
+ val json = JSONObject(response)
+ try {
+ val info = json.getString("Information")
+ if (info.isNotEmpty()) {
+ throw ModuleException(debugMessage, unescapeXml(info))
+ }
+ } catch (ignore: JSONException) {
+ // Do nothing
+ }
+ try {
+ var error = json.getString("Note")
+ if (error.isNotEmpty()) {
+ throw ModuleException(debugMessage, unescapeXml(error))
+ }
+ error = json.getString("Error Message")
+ if (error.isNotEmpty()) {
+ throw ModuleException(debugMessage, unescapeXml(error))
+ }
+ } catch (ignore: JSONException) {
+ // Do nothing
+ }
+ json
+ } else {
+ throw ModuleException(debugMessage, "Empty Response.")
+ }
+ }
+
+ /**
+ * Retrieves a stock quote.
+ */
+ @JvmStatic
+ @Throws(ModuleException::class)
+ fun getQuote(symbol: String, apiKey: String?): List {
+ if (apiKey.isNullOrBlank()) {
+ throw ModuleException(
+ "${StockQuote::class.java.name} is disabled.",
+ "${STOCK_CMD.capitalise()} is disabled. The API key is missing."
+ )
+ }
+ val messages = mutableListOf()
+ if (symbol.isNotBlank()) {
+ val debugMessage = "getQuote($symbol)"
+ var response: String
+ try {
+ with(messages) {
+ // Search for symbol/keywords
+ response = urlReader(
+ URL(
+ "${ALPHAVANTAGE_URL}SYMBOL_SEARCH&keywords=" + encodeUrl(symbol)
+ + "&apikey=" + encodeUrl(apiKey)
+ )
+ )
+ var json = getJsonResponse(response, debugMessage)
+ val symbols = json.getJSONArray("bestMatches")
+ if (symbols.isEmpty) {
+ messages.add(ErrorMessage(INVALID_SYMBOL))
+ } else {
+ val symbolInfo = symbols.getJSONObject(0)
+
+ // Get quote for symbol
+ response = urlReader(
+ URL(
+ "${ALPHAVANTAGE_URL}GLOBAL_QUOTE&symbol="
+ + encodeUrl(symbolInfo.getString("1. symbol"))
+ + "&apikey=" + encodeUrl(apiKey)
+ )
+ )
+ json = getJsonResponse(response, debugMessage)
+ val quote = json.getJSONObject("Global Quote")
+ if (quote.isEmpty) {
+ add(ErrorMessage(INVALID_SYMBOL))
+ } else {
+
+ add(
+ PublicMessage(
+ "Symbol: " + unescapeXml(quote.getString("01. symbol"))
+ + " [" + unescapeXml(symbolInfo.getString("2. name")) + ']'
+ )
+ )
+
+ val pad = 10
+
+ add(
+ PublicMessage(
+ "Price:".padEnd(pad).prependIndent()
+ + unescapeXml(quote.getString("05. price"))
+ )
+ )
+ 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"
+ )
+
+ data.forEach {
+ add(
+ NoticeMessage(
+ "${it.first}:".padEnd(pad).prependIndent()
+ + unescapeXml(quote.getString(it.second))
+ )
+ )
+ }
+
+ add(
+ NoticeMessage(
+ "Change:".padEnd(pad).prependIndent()
+ + unescapeXml(quote.getString("09. change"))
+ + " [" + unescapeXml(quote.getString("10. change percent")) + ']'
+ )
+ )
+ }
+ }
+ }
+ } catch (e: IOException) {
+ throw ModuleException("$debugMessage: IOE", "An IO error has occurred retrieving a stock quote.", e)
+ } catch (e: NullPointerException) {
+ throw ModuleException("$debugMessage: NPE", "An error has occurred retrieving a stock quote.", e)
+ }
+ } else {
+ messages.add(ErrorMessage(INVALID_SYMBOL))
+ }
+ return messages
+ }
+ }
+
+ init {
+ commands.add(STOCK_CMD)
+ help.add("To retrieve a stock quote:")
+ help.add(helpFormat("%c $STOCK_CMD "))
+ initProperties(ALPHAVANTAGE_API_KEY_PROP)
+ }
+}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/modules/ThreadedModule.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/ThreadedModule.kt
new file mode 100644
index 0000000..269b24d
--- /dev/null
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/ThreadedModule.kt
@@ -0,0 +1,58 @@
+/*
+ * ThreadedModule.kt
+ *
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package net.thauvin.erik.mobibot.modules
+
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import org.pircbotx.hooks.types.GenericMessageEvent
+
+/**
+ * The `ThreadedModule` class.
+ */
+abstract class ThreadedModule : AbstractModule() {
+ override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
+ if (isEnabled && event.message.isNotEmpty()) {
+ runBlocking {
+ launch {
+ run(channel, cmd, args, event)
+ }
+ }
+ } else {
+ helpResponse(event)
+ }
+ }
+
+ /**
+ * Runs the thread.
+ */
+ abstract fun run(channel: String, cmd: String, args: String, event: GenericMessageEvent)
+}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/modules/Twitter.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/Twitter.kt
new file mode 100644
index 0000000..74f0ba0
--- /dev/null
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/Twitter.kt
@@ -0,0 +1,246 @@
+/*
+ * Twitter.kt
+ *
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package net.thauvin.erik.mobibot.modules
+
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import net.thauvin.erik.mobibot.Constants
+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 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 : ThreadedModule() {
+ private val logger: Logger = LoggerFactory.getLogger(Twitter::class.java)
+
+ private val timer = Timer(true)
+
+ // Twitter auto-posts.
+ private val entries: MutableSet = HashSet()
+
+ override val name = "Twitter"
+
+ /**
+ * Add an entry to be posted on Twitter.
+ */
+ private fun addEntry(index: Int) {
+ entries.add(index)
+ }
+
+ fun entriesCount(): Int {
+ return entries.size
+ }
+
+ private val handle: String?
+ get() = properties[HANDLE_PROP]
+
+ private fun hasEntry(index: Int): Boolean {
+ return entries.contains(index)
+ }
+
+ val isAutoPost: Boolean
+ get() = isEnabled && properties[AUTOPOST_PROP].toBoolean()
+
+ override val isValidProperties: Boolean
+ get() {
+ for (s in propertyKeys) {
+ if (AUTOPOST_PROP != s && HANDLE_PROP != s && properties[s].isNullOrBlank()) {
+ return false
+ }
+ }
+ return true
+ }
+
+ /**
+ * Send a notification to the registered Twitter handle.
+ */
+ fun notification(msg: String) {
+ 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)
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Posts on Twitter.
+ */
+ @Throws(ModuleException::class)
+ fun post(handle: String = "${properties[HANDLE_PROP]}", message: String, isDm: Boolean): String {
+ return twitterPost(
+ properties[CONSUMER_KEY_PROP],
+ properties[CONSUMER_SECRET_PROP],
+ properties[TOKEN_PROP],
+ properties[TOKEN_SECRET_PROP],
+ handle,
+ message,
+ isDm
+ )
+ }
+
+ /**
+ * Post an entry to twitter.
+ */
+ fun postEntry(index: Int) {
+ 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)
+ }
+ }
+
+ fun queueEntry(index: Int) {
+ if (isAutoPost) {
+ addEntry(index)
+ if (logger.isDebugEnabled) {
+ logger.debug("Scheduling {} for posting on Twitter.", EntriesUtils.buildLinkLabel(index))
+ }
+ timer.schedule(TwitterTimer(this, index), Constants.TIMER_DELAY * 60L * 1000L)
+ }
+ }
+
+ fun removeEntry(index: Int) {
+ entries.remove(index)
+ }
+
+ /**
+ * Posts to twitter.
+ */
+ 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)
+ e.message?.let {
+ event.respond(it)
+ }
+ }
+ }
+
+ /**
+ * Post all the entries to Twitter on shutdown.
+ */
+ fun shutdown() {
+ timer.cancel()
+ if (isAutoPost) {
+ for (index in entries) {
+ postEntry(index)
+ }
+ }
+ }
+
+ companion object {
+ // Property keys
+ const val AUTOPOST_PROP = "twitter-auto-post"
+ const val CONSUMER_KEY_PROP = "twitter-consumerKey"
+ const val CONSUMER_SECRET_PROP = "twitter-consumerSecret"
+ const val HANDLE_PROP = "twitter-handle"
+ const val TOKEN_PROP = "twitter-token"
+ const val TOKEN_SECRET_PROP = "twitter-tokenSecret"
+
+ // Twitter command
+ private const val TWITTER_CMD = "twitter"
+
+ /**
+ * Posts on Twitter.
+ */
+ @JvmStatic
+ @Throws(ModuleException::class)
+ fun twitterPost(
+ consumerKey: String?,
+ consumerSecret: String?,
+ token: String?,
+ tokenSecret: String?,
+ handle: String?,
+ message: String,
+ isDm: Boolean
+ ): String {
+ return try {
+ 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)
+ "Your message was posted to https://twitter.com/${twitter.screenName}/statuses/${status.id}"
+ } else {
+ val dm = twitter.sendDirectMessage(handle, message)
+ dm.text
+ }
+ } catch (e: TwitterException) {
+ throw ModuleException("twitterPost($message)", "An error has occurred: ${e.message}", e)
+ }
+ }
+ }
+
+ init {
+ commands.add(TWITTER_CMD)
+ help.add("To post to Twitter:")
+ help.add(helpFormat("%c $TWITTER_CMD "))
+ properties[AUTOPOST_PROP] = "false"
+ initProperties(CONSUMER_KEY_PROP, CONSUMER_SECRET_PROP, HANDLE_PROP, TOKEN_PROP, TOKEN_SECRET_PROP)
+ }
+}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/modules/Weather2.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/Weather2.kt
new file mode 100644
index 0000000..96ae93e
--- /dev/null
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/Weather2.kt
@@ -0,0 +1,249 @@
+/*
+ * Weather2.kt
+ *
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package net.thauvin.erik.mobibot.modules
+
+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.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.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 : ThreadedModule() {
+ private val logger: Logger = LoggerFactory.getLogger(Weather2::class.java)
+
+ override val name = "Weather"
+
+ /**
+ * Fetches the weather data from a specific city.
+ */
+ override fun run(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
+ if (args.isNotBlank()) {
+ 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.isWarnEnabled) logger.warn(e.debugMessage, e)
+ e.message?.let {
+ event.respond(it)
+ }
+ }
+ } else {
+ helpResponse(event)
+ }
+ }
+
+ companion object {
+ /**
+ * The OpenWeatherMap API Key property.
+ */
+ const val OWM_API_KEY_PROP = "owm-api-key"
+
+ // Weather command
+ private const val WEATHER_CMD = "weather"
+
+ /**
+ * Converts and rounds temperature from °F to °C.
+ */
+ fun ftoC(d: Double?): Pair {
+ val c = (d!! - 32) * 5 / 9
+ return d.roundToInt() to c.roundToInt()
+ }
+
+ /**
+ * Returns a country based on its country code. Defaults to [Country.UNITED_STATES] if not found.
+ */
+ fun getCountry(countryCode: String): Country {
+ for (c in Country.values()) {
+ if (c.value.equals(countryCode, ignoreCase = true)) {
+ return c
+ }
+ }
+ return Country.UNITED_STATES
+ }
+
+ /**
+ * Retrieves the weather data.
+ */
+ @JvmStatic
+ @Throws(ModuleException::class)
+ fun getWeather(query: String, apiKey: String?): List {
+ if (apiKey.isNullOrBlank()) {
+ throw ModuleException(
+ "${Weather2::class.java.name} is disabled.",
+ "${WEATHER_CMD.capitalise()} is disabled. The API key is missing."
+ )
+ }
+ val owm = OWM(apiKey)
+ val messages = mutableListOf()
+ owm.unit = OWM.Unit.IMPERIAL
+ if (query.isNotBlank()) {
+ val argv = query.split(",")
+ if (argv.size in 1..2) {
+ val city = argv[0].trim()
+ val code: String = if (argv.size > 1 && argv[1].isNotBlank()) {
+ argv[1].trim()
+ } else {
+ "US"
+ }
+ try {
+ val country = getCountry(code)
+ val cwd: CurrentWeather = if (city.matches("\\d+".toRegex())) {
+ owm.currentWeatherByZipCode(city.toInt(), country)
+ } else {
+ owm.currentWeatherByCityName(city, country)
+ }
+ if (cwd.hasCityName()) {
+ messages.add(
+ PublicMessage(
+ "City: ${cwd.cityName}, " +
+ country.name.replace('_', ' ').capitalizeWords() + " [${country.value}]"
+ )
+ )
+ cwd.mainData?.let {
+ with(it) {
+ if (hasTemp()) {
+ val t = ftoC(temp)
+ messages.add(PublicMessage("Temperature: ${t.first}°F, ${t.second}°C"))
+ }
+ if (hasHumidity()) {
+ humidity?.let { h ->
+ messages.add(NoticeMessage("Humidity: ${h.roundToInt()}%"))
+ }
+ }
+ }
+ }
+ if (cwd.hasWindData()) {
+ cwd.windData?.let {
+ if (it.hasSpeed()) {
+ it.speed?.let { s ->
+ val w = mphToKmh(s)
+ messages.add(NoticeMessage("Wind: ${w.first} mph, ${w.second} km/h"))
+ }
+ }
+ }
+ }
+ if (cwd.hasWeatherList()) {
+ val condition = StringBuilder("Condition:")
+ cwd.weatherList?.let {
+ for (w in it) {
+ w?.let {
+ condition.append(' ')
+ .append(w.getDescription().capitalise())
+ .append('.')
+ }
+ }
+ messages.add(NoticeMessage(condition.toString()))
+ }
+ }
+ if (cwd.hasCityId()) {
+ cwd.cityId?.let {
+ if (it > 0) {
+ messages.add(
+ NoticeMessage("https://openweathermap.org/city/$it", Colors.GREEN)
+ )
+ } else {
+ messages.add(
+ NoticeMessage(
+ "https://openweathermap.org/find?q="
+ + encodeUrl("$city,${code.uppercase()}"),
+ Colors.GREEN
+ )
+ )
+ }
+ }
+ }
+ }
+ } catch (e: APIException) {
+ if (e.code == 404) {
+ throw ModuleException(
+ "getWeather($query): API ${e.code}",
+ "The requested city was not found.",
+ e
+ )
+ } else {
+ throw ModuleException("getWeather($query): API ${e.code}", e.message, e)
+ }
+ } catch (e: NullPointerException) {
+ throw ModuleException("getWeather($query): NPE", "Unable to perform weather lookup.", e)
+ }
+ }
+ }
+ if (messages.isEmpty()) {
+ messages.add(ErrorMessage("Invalid syntax."))
+ }
+ return messages
+ }
+
+ /**
+ * Converts and rounds temperature from mph to km/h.
+ */
+ fun mphToKmh(w: Double): Pair {
+ val kmh = w * 1.60934
+ return w.roundToInt() to kmh.roundToInt()
+ }
+ }
+
+ init {
+ commands.add(WEATHER_CMD)
+ with(help) {
+ add("To display weather information:")
+ add(helpFormat("%c $WEATHER_CMD [, ]"))
+ add("For example:")
+ add(helpFormat("%c $WEATHER_CMD paris, fr"))
+ add("The default ISO 3166 country code is ${"US".bold()}. Zip codes supported in most countries.")
+ }
+ initProperties(OWM_API_KEY_PROP)
+ }
+}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/modules/WorldTime.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/WorldTime.kt
new file mode 100644
index 0000000..fc0edf6
--- /dev/null
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/WorldTime.kt
@@ -0,0 +1,391 @@
+/*
+ * WorldTime.kt
+ *
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package net.thauvin.erik.mobibot.modules
+
+import net.thauvin.erik.mobibot.Utils.bold
+import net.thauvin.erik.mobibot.Utils.helpFormat
+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
+import java.time.temporal.ChronoField
+
+/**
+ * The WorldTime module.
+ */
+class WorldTime : AbstractModule() {
+ override val name = "WorldTime"
+
+ companion object {
+ /**
+ * Beats (Internet Time) keyword
+ */
+ const val BEATS_KEYWORD = ".beats"
+
+ /**
+ * Supported countries
+ */
+ val COUNTRIES_MAP = buildMap {
+ put("AG", "America/Antigua")
+ put("AI", "America/Anguilla")
+ put("AE", "Asia/Dubai")
+ put("AD", "Europe/Andorra")
+ put("AKDT", "America/Anchorage")
+ put("AF", "Asia/Kabul")
+ put("AKST", "America/Anchorage")
+ put("AL", "Europe/Tirane")
+ put("AM", "Asia/Yerevan")
+ put("AO", "Africa/Luanda")
+ put("AQ", "Antarctica/South_Pole")
+ put("AR", "America/Argentina/Buenos_Aires")
+ put("AS", "Pacific/Pago_Pago")
+ put("AT", "Europe/Vienna")
+ put("AU", "Australia/Sydney")
+ put("AW", "America/Aruba")
+ put("AX", "Europe/Mariehamn")
+ put("AZ", "Asia/Baku")
+ put("BA", "Europe/Sarajevo")
+ put("BB", "America/Barbados")
+ put("BD", "Asia/Dhaka")
+ put("BE", "Europe/Brussels")
+ put("BEAT", BEATS_KEYWORD)
+ put("BF", "Africa/Ouagadougou")
+ put("BG", "Europe/Sofia")
+ put("BH", "Asia/Bahrain")
+ put("BI", "Africa/Bujumbura")
+ put("BJ", "Africa/Porto-Novo")
+ put("BL", "America/St_Barthelemy")
+ put("BM", "Atlantic/Bermuda")
+ put("BMT", BEATS_KEYWORD)
+ put("BN", "Asia/Brunei")
+ put("BO", "America/La_Paz")
+ put("BQ", "America/Kralendijk")
+ put("BR", "America/Sao_Paulo")
+ put("BS", "America/Nassau")
+ put("BT", "Asia/Thimphu")
+ put("BW", "Africa/Gaborone")
+ put("BY", "Europe/Minsk")
+ put("BZ", "America/Belize")
+ put("CA", "America/Montreal")
+ put("CC", "Indian/Cocos")
+ put("CD", "Africa/Kinshasa")
+ put("CDT", "America/Chicago")
+ put("CET", "CET")
+ put("CF", "Africa/Bangui")
+ put("CG", "Africa/Brazzaville")
+ put("CH", "Europe/Zurich")
+ put("CI", "Africa/Abidjan")
+ put("CK", "Pacific/Rarotonga")
+ put("CL", "America/Santiago")
+ put("CM", "Africa/Douala")
+ put("CN", "Asia/Shanghai")
+ put("CO", "America/Bogota")
+ put("CR", "America/Costa_Rica")
+ put("CST", "America/Chicago")
+ put("CU", "Cuba")
+ put("CV", "Atlantic/Cape_Verde")
+ put("CW", "America/Curacao")
+ put("CX", "Indian/Christmas")
+ put("CY", "Asia/Nicosia")
+ put("CZ", "Europe/Prague")
+ put("DE", "Europe/Berlin")
+ put("DJ", "Africa/Djibouti")
+ put("DK", "Europe/Copenhagen")
+ put("DM", "America/Dominica")
+ put("DO", "America/Santo_Domingo")
+ put("DZ", "Africa/Algiers")
+ put("EC", "Pacific/Galapagos")
+ put("EDT", "America/New_York")
+ put("EE", "Europe/Tallinn")
+ put("EG", "Africa/Cairo")
+ put("EH", "Africa/El_Aaiun")
+ put("ER", "Africa/Asmara")
+ put("ES", "Europe/Madrid")
+ put("EST", "America/New_York")
+ put("ET", "Africa/Addis_Ababa")
+ put("FI", "Europe/Helsinki")
+ put("FJ", "Pacific/Fiji")
+ put("FK", "Atlantic/Stanley")
+ put("FM", "Pacific/Yap")
+ put("FO", "Atlantic/Faroe")
+ put("FR", "Europe/Paris")
+ put("GA", "Africa/Libreville")
+ put("GB", "Europe/London")
+ put("GD", "America/Grenada")
+ put("GE", "Asia/Tbilisi")
+ put("GF", "America/Cayenne")
+ put("GG", "Europe/Guernsey")
+ put("GH", "Africa/Accra")
+ put("GI", "Europe/Gibraltar")
+ put("GL", "America/Thule")
+ put("GM", "Africa/Banjul")
+ put("GMT", "GMT")
+ put("GN", "Africa/Conakry")
+ put("GP", "America/Guadeloupe")
+ put("GQ", "Africa/Malabo")
+ put("GR", "Europe/Athens")
+ put("GS", "Atlantic/South_Georgia")
+ put("GT", "America/Guatemala")
+ put("GU", "Pacific/Guam")
+ put("GW", "Africa/Bissau")
+ put("GY", "America/Guyana")
+ put("HK", "Asia/Hong_Kong")
+ put("HN", "America/Tegucigalpa")
+ put("HR", "Europe/Zagreb")
+ put("HST", "Pacific/Honolulu")
+ put("HT", "America/Port-au-Prince")
+ put("HU", "Europe/Budapest")
+ put("ID", "Asia/Jakarta")
+ put("IE", "Europe/Dublin")
+ put("IL", "Asia/Tel_Aviv")
+ put("IM", "Europe/Isle_of_Man")
+ put("IN", "Asia/Kolkata")
+ put("IO", "Indian/Chagos")
+ put("IQ", "Asia/Baghdad")
+ put("IR", "Asia/Tehran")
+ put("IS", "Atlantic/Reykjavik")
+ put("IT", "Europe/Rome")
+ put("JE", "Europe/Jersey")
+ put("JM", "Jamaica")
+ put("JO", "Asia/Amman")
+ put("JP", "Asia/Tokyo")
+ put("KE", "Africa/Nairobi")
+ put("KG", "Asia/Bishkek")
+ put("KH", "Asia/Phnom_Penh")
+ put("KI", "Pacific/Tarawa")
+ put("KM", "Indian/Comoro")
+ put("KN", "America/St_Kitts")
+ put("KP", "Asia/Pyongyang")
+ put("KR", "Asia/Seoul")
+ put("KW", "Asia/Riyadh")
+ put("KY", "America/Cayman")
+ put("KZ", "Asia/Oral")
+ put("LA", "Asia/Vientiane")
+ put("LB", "Asia/Beirut")
+ put("LC", "America/St_Lucia")
+ put("LI", "Europe/Vaduz")
+ put("LK", "Asia/Colombo")
+ put("LR", "Africa/Monrovia")
+ put("LS", "Africa/Maseru")
+ put("LT", "Europe/Vilnius")
+ put("LU", "Europe/Luxembourg")
+ put("LV", "Europe/Riga")
+ put("LY", "Africa/Tripoli")
+ put("MA", "Africa/Casablanca")
+ put("MC", "Europe/Monaco")
+ put("MD", "Europe/Chisinau")
+ put("MDT", "America/Denver")
+ put("ME", "Europe/Podgorica")
+ put("MF", "America/Marigot")
+ put("MG", "Indian/Antananarivo")
+ put("MH", "Pacific/Majuro")
+ put("MK", "Europe/Skopje")
+ put("ML", "Africa/Timbuktu")
+ put("MM", "Asia/Yangon")
+ put("MN", "Asia/Ulaanbaatar")
+ put("MO", "Asia/Macau")
+ put("MP", "Pacific/Saipan")
+ put("MQ", "America/Martinique")
+ put("MR", "Africa/Nouakchott")
+ put("MS", "America/Montserrat")
+ put("MST", "America/Denver")
+ put("MT", "Europe/Malta")
+ put("MU", "Indian/Mauritius")
+ put("MV", "Indian/Maldives")
+ put("MW", "Africa/Blantyre")
+ put("MX", "America/Mexico_City")
+ put("MY", "Asia/Kuala_Lumpur")
+ put("MZ", "Africa/Maputo")
+ put("NA", "Africa/Windhoek")
+ put("NC", "Pacific/Noumea")
+ put("NE", "Africa/Niamey")
+ put("NF", "Pacific/Norfolk")
+ put("NG", "Africa/Lagos")
+ put("NI", "America/Managua")
+ put("NL", "Europe/Amsterdam")
+ put("NO", "Europe/Oslo")
+ put("NP", "Asia/Kathmandu")
+ put("NR", "Pacific/Nauru")
+ put("NU", "Pacific/Niue")
+ put("NZ", "Pacific/Auckland")
+ put("OM", "Asia/Muscat")
+ put("PA", "America/Panama")
+ put("PDT", "America/Los_Angeles")
+ put("PE", "America/Lima")
+ put("PF", "Pacific/Tahiti")
+ put("PG", "Pacific/Port_Moresby")
+ put("PH", "Asia/Manila")
+ put("PK", "Asia/Karachi")
+ put("PL", "Europe/Warsaw")
+ put("PM", "America/Miquelon")
+ put("PN", "Pacific/Pitcairn")
+ put("PR", "America/Puerto_Rico")
+ put("PS", "Asia/Gaza")
+ put("PST", "America/Los_Angeles")
+ put("PT", "Europe/Lisbon")
+ put("PW", "Pacific/Palau")
+ put("PY", "America/Asuncion")
+ put("QA", "Asia/Qatar")
+ put("RE", "Indian/Reunion")
+ put("RO", "Europe/Bucharest")
+ put("RS", "Europe/Belgrade")
+ put("RU", "Europe/Moscow")
+ put("RW", "Africa/Kigali")
+ put("SA", "Asia/Riyadh")
+ put("SB", "Pacific/Guadalcanal")
+ put("SC", "Indian/Mahe")
+ put("SD", "Africa/Khartoum")
+ put("SE", "Europe/Stockholm")
+ put("SG", "Asia/Singapore")
+ put("SH", "Atlantic/St_Helena")
+ put("SI", "Europe/Ljubljana")
+ put("SJ", "Atlantic/Jan_Mayen")
+ put("SK", "Europe/Bratislava")
+ put("SL", "Africa/Freetown")
+ put("SM", "Europe/San_Marino")
+ put("SN", "Africa/Dakar")
+ put("SO", "Africa/Mogadishu")
+ put("SR", "America/Paramaribo")
+ put("SS", "Africa/Juba")
+ put("ST", "Africa/Sao_Tome")
+ put("SV", "America/El_Salvador")
+ put("SX", "America/Lower_Princes")
+ put("SY", "Asia/Damascus")
+ put("SZ", "Africa/Mbabane")
+ put("TC", "America/Grand_Turk")
+ put("TD", "Africa/Ndjamena")
+ put("TF", "Indian/Kerguelen")
+ put("TG", "Africa/Lome")
+ put("TH", "Asia/Bangkok")
+ put("TJ", "Asia/Dushanbe")
+ put("TK", "Pacific/Fakaofo")
+ put("TL", "Asia/Dili")
+ put("TM", "Asia/Ashgabat")
+ put("TN", "Africa/Tunis")
+ put("TO", "Pacific/Tongatapu")
+ put("TR", "Europe/Istanbul")
+ put("TT", "America/Port_of_Spain")
+ put("TV", "Pacific/Funafuti")
+ put("TW", "Asia/Taipei")
+ put("TZ", "Africa/Dar_es_Salaam")
+ put("UA", "Europe/Kiev")
+ put("UG", "Africa/Kampala")
+ put("UK", "Europe/London")
+ put("UM", "Pacific/Wake")
+ put("US", "America/New_York")
+ put("UTC", "UTC")
+ put("UY", "America/Montevideo")
+ put("UZ", "Asia/Tashkent")
+ put("VA", "Europe/Vatican")
+ put("VC", "America/St_Vincent")
+ put("VE", "America/Caracas")
+ put("VG", "America/Tortola")
+ put("VI", "America/St_Thomas")
+ put("VN", "Asia/Ho_Chi_Minh")
+ put("VU", "Pacific/Efate")
+ put("WF", "Pacific/Wallis")
+ put("WS", "Pacific/Apia")
+ put("YE", "Asia/Aden")
+ put("YT", "Indian/Mayotte")
+ put("ZA", "Africa/Johannesburg")
+ put("ZM", "Africa/Lusaka")
+ put("ZULU", "Zulu")
+ put("ZW", "Africa/Harare")
+ ZoneId.getAvailableZoneIds().filter { it.length <= 3 && !containsKey(it) }
+ .forEach { tz -> put(tz, tz) }
+ }
+
+ // 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 ${"'HH:mm'".bold()} on ${"'EEEE, d MMMM yyyy'".bold()} in '")
+
+ /**
+ * Returns the current Internet (beat) Time.
+ */
+ 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 "%c%03d".format('@', beats)
+ }
+
+ /**
+ * Returns the time for the given timezone/city.
+ */
+ @JvmStatic
+ 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 ${internetTime().bold()} $BEATS_KEYWORD"
+ } else {
+ (ZonedDateTime.now().withZoneSameInstant(ZoneId.of(tz)).format(dtf)
+ + tz.substring(tz.lastIndexOf('/') + 1).replace('_', ' ').bold())
+ }
+ } else {
+ "Unsupported country/zone. Please try again."
+ }
+ }
+ }
+
+ 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))
+ }
+ }
+
+ override val isPrivateMsgEnabled = true
+
+ init {
+ with(help) {
+ add("To display a country's current date/time:")
+ add(helpFormat("%c $TIME_CMD []"))
+ add("For a listing of the supported countries/zones:")
+ add(helpFormat("%c $TIME_CMD $ZONES_ARGS"))
+ }
+ commands.add(TIME_CMD)
+ }
+}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/msg/ErrorMessage.kt b/src/main/kotlin/net/thauvin/erik/mobibot/msg/ErrorMessage.kt
new file mode 100644
index 0000000..ae5651c
--- /dev/null
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/msg/ErrorMessage.kt
@@ -0,0 +1,38 @@
+/*
+ * ErrorMessage.kt
+ *
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package net.thauvin.erik.mobibot.msg
+
+/**
+ * The `ErrorMessage` class.
+ */
+class ErrorMessage @JvmOverloads constructor(msg: String, color: String = DEFAULT_COLOR) :
+ Message(msg, color, isError = true)
diff --git a/src/test/java/net/thauvin/erik/mobibot/modules/WordTimeTest.java b/src/main/kotlin/net/thauvin/erik/mobibot/msg/Message.kt
similarity index 65%
rename from src/test/java/net/thauvin/erik/mobibot/modules/WordTimeTest.java
rename to src/main/kotlin/net/thauvin/erik/mobibot/msg/Message.kt
index b7cfc43..2e51c02 100644
--- a/src/test/java/net/thauvin/erik/mobibot/modules/WordTimeTest.java
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/msg/Message.kt
@@ -1,7 +1,7 @@
/*
- * WordTimeTest.java
+ * Message.kt
*
- * Copyright (c) 2004-2019, Erik C. Thauvin (erik@thauvin.net)
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -29,30 +29,38 @@
* 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.msg
-package net.thauvin.erik.mobibot.modules;
-
-import org.testng.annotations.Test;
-
-import static org.assertj.core.api.Assertions.assertThat;
+import net.thauvin.erik.semver.Constants
/**
- * The WordTimeTest class.
- *
- * @author Erik C. Thauvin
- * @created 2019-04-07
- * @since 1.0
+ * The `Message` class.
*/
-public class WordTimeTest {
- @Test
- public void testWorldTime() {
- assertThat(WorldTime.worldTime("PST").getMessage()).as("PST").endsWith("Los Angeles");
- assertThat(WorldTime.worldTime("BLAH").isError()).as("BLAH").isTrue();
- assertThat(WorldTime.worldTime("BEATS").getMessage()).as("BEATS").contains("@");
+open class Message @JvmOverloads constructor(
+ var msg: String,
+ var color: String = DEFAULT_COLOR,
+ var isNotice: Boolean = false,
+ isError: Boolean = false,
+ var isPrivate: Boolean = false
+) {
+ companion object {
+ var DEFAULT_COLOR = Constants.EMPTY
}
- @Test
- public void testWorldTimeImpl() {
- AbstractModuleTest.testAbstractModule(new Lookup());
+ init {
+ if (isError) {
+ isNotice = true
+ }
+ }
+
+ /** Error flag. */
+ var isError = isError
+ set(value) {
+ if (value) isNotice = value
+ field = value
+ }
+
+ override fun toString(): String {
+ return "Message(color='$color', isError=$isError, isNotice=$isNotice, isPrivate=$isPrivate, msg='$msg')"
}
}
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/msg/NoticeMessage.kt b/src/main/kotlin/net/thauvin/erik/mobibot/msg/NoticeMessage.kt
new file mode 100644
index 0000000..f88b8dd
--- /dev/null
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/msg/NoticeMessage.kt
@@ -0,0 +1,39 @@
+/*
+ * NoticeMessage.kt
+ *
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package net.thauvin.erik.mobibot.msg
+
+/**
+ * The `NoticeMessage` class.
+ */
+class NoticeMessage @JvmOverloads constructor(msg: String, color: String = DEFAULT_COLOR) :
+ Message(msg, color, isNotice = true)
+
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/msg/PrivateMessage.kt b/src/main/kotlin/net/thauvin/erik/mobibot/msg/PrivateMessage.kt
new file mode 100644
index 0000000..827b682
--- /dev/null
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/msg/PrivateMessage.kt
@@ -0,0 +1,39 @@
+/*
+ * PrivateMessage.kt
+ *
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package net.thauvin.erik.mobibot.msg
+
+/**
+ * The `PrivateMessage` class.
+ */
+@Suppress("unused")
+class PrivateMessage @JvmOverloads constructor(msg: String, color: String = DEFAULT_COLOR) :
+ Message(msg, color, isPrivate = true)
diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/msg/PublicMessage.kt b/src/main/kotlin/net/thauvin/erik/mobibot/msg/PublicMessage.kt
new file mode 100644
index 0000000..71b2a5b
--- /dev/null
+++ b/src/main/kotlin/net/thauvin/erik/mobibot/msg/PublicMessage.kt
@@ -0,0 +1,37 @@
+/*
+ * PublicMessage.kt
+ *
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package net.thauvin.erik.mobibot.msg
+
+/**
+ * The `PublicMessage` class.
+ */
+class PublicMessage @JvmOverloads constructor(msg: String, color: String = DEFAULT_COLOR) : Message(msg, color)
diff --git a/src/test/java/net/thauvin/erik/mobibot/UtilsTest.java b/src/test/java/net/thauvin/erik/mobibot/UtilsTest.java
deleted file mode 100644
index df2ff57..0000000
--- a/src/test/java/net/thauvin/erik/mobibot/UtilsTest.java
+++ /dev/null
@@ -1,152 +0,0 @@
-/*
- * UtilsTest.java
- *
- * Copyright (c) 2004-2019, 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;
-
-import org.apache.commons.lang3.StringUtils;
-import org.jibble.pircbot.Colors;
-import org.testng.annotations.BeforeClass;
-import org.testng.annotations.Test;
-
-import java.io.File;
-import java.time.LocalDateTime;
-import java.util.Calendar;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-/**
- * The Utils Test class.
- *
- * @author Erik C. Thauvin
- * @created 2017-05-30
- * @since 1.0
- */
-public class UtilsTest {
- private static final String ASCII =
- " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~";
-
- private final Calendar cal = Calendar.getInstance();
- private final LocalDateTime localDateTime = LocalDateTime.of(1952, 2, 17, 12, 30, 0);
-
- @BeforeClass
- public void setUp() {
- cal.set(1952, Calendar.FEBRUARY, 17, 12, 30, 0);
- }
-
- @Test
- public void testBold() {
- assertThat(Utils.bold(1)).as("bold(1)").isEqualTo(Colors.BOLD + "1" + Colors.BOLD);
- assertThat(Utils.bold(ASCII)).as("bold(ascii").isEqualTo(Colors.BOLD + ASCII + Colors.BOLD);
- }
-
- @Test
- public void testColorize() {
- assertThat(Utils.colorize(ASCII, Colors.REVERSE)).as("colorize(reverse)").isEqualTo(
- Colors.REVERSE + ASCII + Colors.REVERSE);
- assertThat(Utils.colorize(ASCII, Colors.RED)).as("colorize(red)").isEqualTo(Colors.RED + ASCII + Colors.NORMAL);
- assertThat(Utils.colorize(null, Colors.RED)).as("colorize(null)").isEqualTo(Colors.NORMAL);
- }
-
- @Test
- public void testCyan() {
- assertThat(Utils.cyan(ASCII)).isEqualTo(Colors.CYAN + ASCII + Colors.NORMAL);
- }
-
- @Test
- public void testEnsureDir() {
- assertThat(Utils.ensureDir("dir", false)).as("ensureDir(dir, false)").isEqualTo("dir" + File.separatorChar);
- assertThat(Utils.ensureDir("https://erik.thauvin.net", true)).as("ensureDir(erik.thauvin.net, true)").isEqualTo(
- "https://erik.thauvin.net/");
- }
-
- @Test
- public void testGetIntProperty() {
- assertThat(Utils.getIntProperty("10", 1)).as("getIntProperty(10, 1)").isEqualTo(10);
- assertThat(Utils.getIntProperty("a", 1)).as("getIntProperty(a, 1)").isEqualTo(1);
- }
-
- @Test
- public void testGreen() {
- assertThat(Utils.green(ASCII)).isEqualTo(Colors.DARK_GREEN + ASCII + Colors.NORMAL);
- }
-
- @Test
- public void testIsoLocalDate() {
- assertThat(Utils.isoLocalDate(cal.getTime())).as("isoLocalDate(date)").isEqualTo("1952-02-17");
- assertThat(Utils.isoLocalDate(localDateTime)).as("isoLocalDate(localDate)").isEqualTo("1952-02-17");
- }
-
- @Test
- public void testObfuscate() {
- assertThat(Utils.obfuscate(ASCII).length()).as("obfuscate is right length").isEqualTo(ASCII.length());
- assertThat(Utils.obfuscate(ASCII)).as("obfuscate()").isEqualTo(StringUtils.repeat("x", ASCII.length()));
- assertThat(Utils.obfuscate(" ")).as("obfuscate(blank)").isEqualTo(" ");
- }
-
- @Test
- public void testPlural() {
- final String week = "week";
- final String weeks = "weeks";
-
- assertThat(Utils.plural(-1, week, weeks)).as("plural(-1)").isEqualTo(week);
- assertThat(Utils.plural(0, week, weeks)).as("plural(0)").isEqualTo(week);
- assertThat(Utils.plural(1, week, weeks)).as("plural(1)").isEqualTo(week);
- assertThat(Utils.plural(2, week, weeks)).as("plural(2)").isEqualTo(weeks);
- }
-
- @Test
- public void testReverseColor() {
- assertThat(Utils.reverseColor(ASCII)).isEqualTo(Colors.REVERSE + ASCII + Colors.REVERSE);
- }
-
- @Test
- public void testToday() {
- assertThat(Utils.today()).isEqualTo(Utils.isoLocalDate(LocalDateTime.now()));
- }
-
- @Test
- public void testUnescapeXml() {
- assertThat(Utils.unescapeXml("<a name="test & ''">")).isEqualTo(
- "");
- }
-
- @Test
- public void testUptime() {
- assertThat("17 years 2 months 2 weeks 1 day 6 hours 45 minutes").isEqualTo(Utils.uptime(547800300076L));
- }
-
- @Test
- public void testUtcDateTime() {
- assertThat(Utils.utcDateTime(cal.getTime())).as("utcDateTime(date)").isEqualTo("1952-02-17 12:30");
- assertThat(Utils.utcDateTime(localDateTime)).as("utcDateTime(localDate)").isEqualTo("1952-02-17 12:30");
- }
-}
diff --git a/src/test/java/net/thauvin/erik/mobibot/entries/EntryLinkTest.java b/src/test/java/net/thauvin/erik/mobibot/entries/EntryLinkTest.java
deleted file mode 100644
index cc4ad92..0000000
--- a/src/test/java/net/thauvin/erik/mobibot/entries/EntryLinkTest.java
+++ /dev/null
@@ -1,100 +0,0 @@
-/*
- * EntryLinkTest.java
- *
- * Copyright (c) 2004-2020, 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.SyndCategory;
-import org.testng.annotations.Test;
-
-import java.security.SecureRandom;
-import java.util.List;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-/**
- * The EntryUtilsTest class.
- *
- * @author Erik C. Thauvin
- * @created 2019-04-19
- * @since 1.0
- */
-public class EntryLinkTest {
- private final EntryLink entryLink = new EntryLink("https://www.mobitopia.org/", "Mobitopia", "Skynx",
- "JimH", "#mobitopia", "tag1, tag2,tag3 TAG4 tag5");
-
-
- @Test
- public void testAddDeleteComment() {
- int i = 0;
- for (; i < 5; i++) {
- entryLink.addComment("c" + i, "u" + i);
- }
-
- i = 0;
- for (final EntryComment comment : entryLink.getComments()) {
- assertThat(comment.getComment()).as("getComment(" + i + ')').isEqualTo("c" + i);
- assertThat(comment.getNick()).as("getNick(" + i + ')').isEqualTo("u" + i);
- i++;
- }
-
- final SecureRandom r = new SecureRandom();
- for (i = 0; entryLink.getCommentsCount() > 0; i++) {
- entryLink.deleteComment(r.nextInt(entryLink.getCommentsCount()));
- }
- assertThat(entryLink.hasComments()).as("hasComments()").isFalse();
-
- entryLink.addComment("nothing", "nobody");
- entryLink.setComment(0, "something", "somebody");
- assertThat(entryLink.getComment(0).getNick()).as("getNick(somebody)").isEqualTo("somebody");
- assertThat(entryLink.getComment(0).getComment()).as("getComment(something)").isEqualTo("something");
-
- }
-
- @Test
- public void testTags() {
- final List tags = entryLink.getTags();
-
- int i = 0;
- for (final SyndCategory tag : tags) {
- assertThat(tag.getName()).as("tag.getName(" + i + ')').isEqualTo("tag" + (i + 1));
- i++;
- }
-
- entryLink.setTags("-tag5");
- entryLink.setTags("+mobitopia");
- entryLink.setTags("tag4");
- entryLink.setTags("-mobitopia");
-
- assertThat(entryLink.getPinboardTags()).as("getPinboardTags()")
- .isEqualTo(entryLink.getNick() + ",tag1,tag2,tag3,tag4,mobitopia");
- }
-}
diff --git a/src/test/java/net/thauvin/erik/mobibot/modules/CurrencyConverterTest.java b/src/test/java/net/thauvin/erik/mobibot/modules/CurrencyConverterTest.java
deleted file mode 100644
index a437408..0000000
--- a/src/test/java/net/thauvin/erik/mobibot/modules/CurrencyConverterTest.java
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * CurrencyConverterTest.java
- *
- * Copyright (c) 2004-2019, 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.modules;
-
-import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
-import net.thauvin.erik.mobibot.msg.ErrorMessage;
-import org.testng.annotations.Test;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-/**
- * The CurrencyConvertTest class.
- *
- * @author Erik C. Thauvin
- * @created 2019-04-07
- * @since 1.0
- */
-public class CurrencyConverterTest {
- @Test
- public void testCurrencyConvertererImpl() {
- AbstractModuleTest.testAbstractModule(new CurrencyConverter());
- }
-
- @Test(expectedExceptions = ModuleException.class)
- public void testException() throws ModuleException {
- CurrencyConverter.convertCurrency("100 BLA to USD");
- }
-
- @Test
- @SuppressFBWarnings("PRMC_POSSIBLY_REDUNDANT_METHOD_CALLS")
- public void testConvertCurrency() throws ModuleException {
- assertThat(CurrencyConverter.convertCurrency("100 USD to EUR").getMessage())
- .as("100 USD to EUR").startsWith("100.00 USD = ");
- assertThat(CurrencyConverter.convertCurrency("100 USD to USD"))
- .as("100 USD to USD").isInstanceOf(ErrorMessage.class);
- assertThat(CurrencyConverter.convertCurrency(CurrencyConverter.CURRENCY_RATES_KEYWORD).isNotice())
- .as(CurrencyConverter.CURRENCY_RATES_KEYWORD + " is notice").isTrue();
- assertThat(CurrencyConverter.convertCurrency(CurrencyConverter.CURRENCY_RATES_KEYWORD).getMessage())
- .as(CurrencyConverter.CURRENCY_RATES_KEYWORD).contains("USD: ");
- }
-}
diff --git a/src/test/java/net/thauvin/erik/mobibot/modules/GoogleSearchTest.java b/src/test/java/net/thauvin/erik/mobibot/modules/GoogleSearchTest.java
deleted file mode 100644
index 13388bd..0000000
--- a/src/test/java/net/thauvin/erik/mobibot/modules/GoogleSearchTest.java
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * GoogleSearchTest.java
- *
- * Copyright (c) 2004-2020, 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.modules;
-
-import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
-import net.thauvin.erik.mobibot.msg.Message;
-import org.testng.annotations.Test;
-
-import java.util.List;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
-
-/**
- * The GoogleSearchTest class.
- *
- * @author Erik C. Thauvin
- * @created 2019-04-08
- * @since 1.0
- */
-public class GoogleSearchTest extends LocalProperties {
- @Test
- public void testGoogleSearchImpl() {
- AbstractModuleTest.testAbstractModule(new GoogleSearch());
- }
-
- @SuppressFBWarnings("LEST_LOST_EXCEPTION_STACK_TRACE")
- @SuppressWarnings("PMD.PreserveStackTrace")
- @Test
- public void testSearchGoogle() throws ModuleException {
- final String apiKey = LocalProperties.getProperty(GoogleSearch.GOOGLE_API_KEY_PROP);
- final String cseKey = LocalProperties.getProperty(GoogleSearch.GOOGLE_CSE_KEY_PROP);
- try {
- List messages = GoogleSearch.searchGoogle("mobibot site:github.com", apiKey, cseKey);
- assertThat(messages).as("mobibot results not empty").isNotEmpty();
- assertThat(messages.get(0).getMessage()).as("found mobitopia").contains("mobibot");
-
- messages = GoogleSearch.searchGoogle("aapl", apiKey, cseKey);
- assertThat(messages).as("aapl results not empty").isNotEmpty();
- assertThat(messages.get(0).getMessage()).as("found apple").containsIgnoringCase("apple");
-
- assertThatThrownBy(() -> GoogleSearch.searchGoogle("test", "", "apiKey")).as("no API key").isInstanceOf(
- ModuleException.class).hasNoCause();
-
- assertThatThrownBy(() -> GoogleSearch.searchGoogle("test", "apiKey", "")).as("no CSE API key").isInstanceOf(
- ModuleException.class).hasNoCause();
-
- assertThatThrownBy(() -> GoogleSearch.searchGoogle("", "apikey", "apiKey")).as("no query").isInstanceOf(
- ModuleException.class).hasNoCause();
- } catch (ModuleException e) {
- // Avoid displaying api keys in CI logs
- if ("true".equals(System.getenv("CI"))) {
- throw new ModuleException(e.getDebugMessage(), e.getSanitizedMessage(apiKey, cseKey));
- } else {
- throw e;
- }
- }
- }
-}
diff --git a/src/test/java/net/thauvin/erik/mobibot/modules/ModuleExceptionTest.java b/src/test/java/net/thauvin/erik/mobibot/modules/ModuleExceptionTest.java
deleted file mode 100644
index 46199c9..0000000
--- a/src/test/java/net/thauvin/erik/mobibot/modules/ModuleExceptionTest.java
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- * ModuleExceptionTest.java
- *
- * Copyright (c) 2004-2019, 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.modules;
-
-import org.testng.annotations.DataProvider;
-import org.testng.annotations.Test;
-
-import java.io.IOException;
-import java.lang.reflect.Method;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-/**
- * The ModuleExceptionTest class.
- *
- * @author Erik C. Thauvin
- * @created 2019-04-09
- * @since 1.0
- */
-public class ModuleExceptionTest {
- static final String debugMessage = "debugMessage";
- static final String message = "message";
-
- @DataProvider(name = "dp")
- Object[][] createData(final Method m) {
- return new Object[][]{new Object[]{new ModuleException(debugMessage,
- message,
- new IOException("URL http://foobar.com"))},
- new Object[]{new ModuleException(debugMessage,
- message,
- new IOException("URL http://foobar.com?"))},
- new Object[]{new ModuleException(debugMessage, message)}};
- }
-
- @Test(dataProvider = "dp")
- final void testGetDebugMessage(final ModuleException e) {
- assertThat(e.getDebugMessage()).as("get debug message").isEqualTo(debugMessage);
- }
-
- @Test(dataProvider = "dp")
- final void testGetMessage(final ModuleException e) {
- assertThat(e.getMessage()).as("get message").isEqualTo(message);
- }
-
- @Test
- final void testGetSanitizedMessage() {
- final String apiKey = "1234567890";
- final ModuleException e = new ModuleException(debugMessage,
- message,
- new IOException(
- "URL http://foo.com?apiKey=" + apiKey + "&userID=me"));
- assertThat(e.getSanitizedMessage(apiKey)).as("sanitized url").contains("xxxxxxxxxx").doesNotContain(apiKey);
- }
-}
diff --git a/src/test/java/net/thauvin/erik/mobibot/modules/StockQuoteTest.java b/src/test/java/net/thauvin/erik/mobibot/modules/StockQuoteTest.java
deleted file mode 100644
index b3a4971..0000000
--- a/src/test/java/net/thauvin/erik/mobibot/modules/StockQuoteTest.java
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * StockQuoteTest.java
- *
- * Copyright (c) 2004-2020, 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.modules;
-
-import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
-import net.thauvin.erik.mobibot.msg.Message;
-import org.testng.annotations.Test;
-
-import java.util.List;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
-
-/**
- * The StockQuoteTest class.
- *
- * @author Erik C. Thauvin
- * @created 2019-04-07
- * @since 1.0
- */
-
-public class StockQuoteTest extends LocalProperties {
- @SuppressFBWarnings("LEST_LOST_EXCEPTION_STACK_TRACE")
- @SuppressWarnings("PMD.PreserveStackTrace")
- @Test
- public void testGetQuote() throws ModuleException {
- final String apiKey = LocalProperties.getProperty(StockQuote.ALPHAVANTAGE_API_KEY_PROP);
- try {
- final List messages = StockQuote.getQuote("apple inc", apiKey);
- assertThat(messages).as("response not empty").isNotEmpty();
- assertThat(messages.get(0).getMessage()).as("same stock symbol").contains("AAPL").contains("Apple Inc.");
-
- try {
- StockQuote.getQuote("012", apiKey);
- } catch (ModuleException e) {
- assertThat(e.getMessage()).as("invalid symbol").containsIgnoringCase(StockQuote.INVALID_SYMBOL);
- }
-
- assertThatThrownBy(() -> StockQuote.getQuote("test", "")).as("no API key").isInstanceOf(
- ModuleException.class).hasNoCause();
-
- assertThatThrownBy(() -> StockQuote.getQuote("", "apikey")).as("no symbol").isInstanceOf(
- ModuleException.class).hasNoCause();
-
- } catch (ModuleException e) {
- // Avoid displaying api keys in CI logs
- if ("true".equals(System.getenv("CI"))) {
- throw new ModuleException(e.getDebugMessage(), e.getSanitizedMessage(apiKey));
- } else {
- throw e;
- }
- }
- }
-
- @Test
- public void testStockQuoteImpl() {
- AbstractModuleTest.testAbstractModule(new StockQuote());
- }
-}
diff --git a/src/test/java/net/thauvin/erik/mobibot/modules/TwitterTest.java b/src/test/java/net/thauvin/erik/mobibot/modules/TwitterTest.java
deleted file mode 100644
index cf64b4d..0000000
--- a/src/test/java/net/thauvin/erik/mobibot/modules/TwitterTest.java
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * TwitterTest.java
- *
- * Copyright (c) 2004-2019, 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.modules;
-
-import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
-import net.thauvin.erik.mobibot.Constants;
-import org.testng.annotations.Test;
-
-import java.net.InetAddress;
-import java.net.UnknownHostException;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-/**
- * The TwitterTest class.
- *
- * @author Erik C. Thauvin
- * @created 2019-04-19
- * @since 1.0
- */
-public class TwitterTest {
- @SuppressFBWarnings("MDM")
- private String getCi() {
- if ("true".equals(System.getenv("CIRCLECI"))) {
- return "CircleCI";
- } else if ("true".equals(System.getenv("TRAVIS"))) {
- return "Travis CI";
- } else {
- try {
- return InetAddress.getLocalHost().getHostName();
- } catch (UnknownHostException e) {
- return "Unknown Host";
- }
- }
- }
-
- @Test
- public void testPostTwitter() throws ModuleException {
- final String msg = "Testing Twitter API from " + getCi();
- assertThat(Twitter.twitterPost(
- LocalProperties.getProperty(Twitter.CONSUMER_KEY_PROP),
- LocalProperties.getProperty(Twitter.CONSUMER_SECRET_PROP),
- LocalProperties.getProperty(Twitter.TOKEN_PROP),
- LocalProperties.getProperty(Twitter.TOKEN_SECRET_PROP),
- LocalProperties.getProperty(Constants.TWITTER_HANDLE_PROP),
- msg,
- true).getMessage()).as("twitterPost(" + msg + ')').isEqualTo(msg);
- }
-}
diff --git a/src/test/java/net/thauvin/erik/mobibot/modules/Weather2Test.java b/src/test/java/net/thauvin/erik/mobibot/modules/Weather2Test.java
deleted file mode 100644
index f39c459..0000000
--- a/src/test/java/net/thauvin/erik/mobibot/modules/Weather2Test.java
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * Weather2Test.java
- *
- * Copyright (c) 2004-2019, 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.modules;
-
-import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
-import net.aksingh.owmjapis.api.APIException;
-import net.thauvin.erik.mobibot.msg.Message;
-import org.testng.annotations.Test;
-
-import java.util.List;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
-
-/**
- * The Weather2Test class.
- *
- * @author Erik C. Thauvin
- * @created 2019-04-09
- * @since 1.0
- */
-public class Weather2Test extends LocalProperties {
- @SuppressFBWarnings("PRMC_POSSIBLY_REDUNDANT_METHOD_CALLS")
- @Test
- public void testWeather() throws ModuleException {
- List messages = Weather2.getWeather("98204", LocalProperties.getProperty(Weather2.OWM_API_KEY_PROP));
- assertThat(messages.get(0).getMessage()).as("is Everett").contains("Everett").contains("US");
- assertThat(messages.get(messages.size() - 1).getMessage()).as("is City Search").endsWith("98204%2CUS");
-
- messages = Weather2.getWeather("London, UK", LocalProperties.getProperty(Weather2.OWM_API_KEY_PROP));
- assertThat(messages.get(0).getMessage()).as("is UK").contains("London").contains("UK");
- assertThat(messages.get(messages.size() - 1).getMessage()).as("is City Code").endsWith("4517009");
-
- assertThatThrownBy(
- () -> Weather2.getWeather("Montpellier, FR", LocalProperties.getProperty(Weather2.OWM_API_KEY_PROP))).as(
- "Montpellier not found").hasCauseInstanceOf(APIException.class);
-
- assertThatThrownBy(
- () -> Weather2.getWeather("test", "")).as(
- "no API key").isInstanceOf(ModuleException.class).hasNoCause();
-
- messages = Weather2.getWeather("", "apikey");
- assertThat(messages.get(0).isError()).as("no query").isTrue();
-
- }
-
- @Test
- public void testWeather2Impl() {
- AbstractModuleTest.testAbstractModule(new Weather2());
- }
-}
diff --git a/src/test/java/net/thauvin/erik/mobibot/tell/TellMessageTest.java b/src/test/java/net/thauvin/erik/mobibot/tell/TellMessageTest.java
deleted file mode 100644
index 04927a8..0000000
--- a/src/test/java/net/thauvin/erik/mobibot/tell/TellMessageTest.java
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * TellMessageTest.java
- *
- * Copyright (c) 2004-2020, 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.tell;
-
-import org.testng.annotations.Test;
-
-import java.time.Duration;
-import java.time.LocalDateTime;
-import java.time.temporal.Temporal;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-/**
- * The TellMessageTest class.
- *
- * @author Erik C. Thauvin
- * @created 2019-07-29
- * @since 1.0
- */
-public class TellMessageTest {
- private boolean isValidDate(final Temporal date) {
- return Duration.between(date, LocalDateTime.now()).toMinutes() < 1;
- }
-
- @Test
- void testTellMessage() {
- final String message = "Test message.";
- final String recipient = "recipient";
- final String sender = "sender";
- final TellMessage tellMessage = new TellMessage(sender, recipient, message);
-
- assertThat(tellMessage.getSender()).as(sender).isEqualTo(sender);
- assertThat(tellMessage.getRecipient()).as(recipient).isEqualTo(recipient);
- assertThat(tellMessage.getMessage()).as(message).isEqualTo(message);
- assertThat(isValidDate(tellMessage.getQueued())).as("queued is valid date/time").isTrue();
-
- assertThat(tellMessage.isMatch(sender)).as("match sender").isTrue();
- assertThat(tellMessage.isMatch(recipient)).as("match recipient").isTrue();
- assertThat(tellMessage.isMatch("foo")).as("foo is no match").isFalse();
-
- assertThat(tellMessage.isMatchId(tellMessage.getId())).as("is match ID").isTrue();
-
- tellMessage.setIsReceived();
- assertThat(tellMessage.isReceived()).as("is received").isTrue();
- assertThat(isValidDate(tellMessage.getReceived())).as("received is valid date/time").isTrue();
-
- tellMessage.setIsNotified();
- assertThat(tellMessage.isNotified()).as("is notified").isTrue();
- }
-}
-
diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/AddonsTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/AddonsTest.kt
new file mode 100644
index 0000000..f03a2c2
--- /dev/null
+++ b/src/test/kotlin/net/thauvin/erik/mobibot/AddonsTest.kt
@@ -0,0 +1,92 @@
+/*
+ * AddonsTest.kt
+ *
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package net.thauvin.erik.mobibot
+
+import assertk.assertThat
+import assertk.assertions.containsExactly
+import assertk.assertions.isEqualTo
+import net.thauvin.erik.mobibot.commands.ChannelFeed
+import net.thauvin.erik.mobibot.commands.Cycle
+import net.thauvin.erik.mobibot.commands.Die
+import net.thauvin.erik.mobibot.commands.Ignore
+import net.thauvin.erik.mobibot.commands.links.Comment
+import net.thauvin.erik.mobibot.commands.links.View
+import net.thauvin.erik.mobibot.modules.Dice
+import net.thauvin.erik.mobibot.modules.Joke
+import net.thauvin.erik.mobibot.modules.Lookup
+import net.thauvin.erik.mobibot.modules.RockPaperScissors
+import net.thauvin.erik.mobibot.modules.Twitter
+import net.thauvin.erik.mobibot.modules.War
+import org.testng.annotations.Test
+import java.util.Properties
+
+class AddonsTest {
+ private val p = Properties().apply {
+ put("disabled-modules", "war,dice Lookup")
+ put("disabled-commands", "View | comment")
+ }
+ private val addons = Addons(p)
+
+ @Test
+ fun addTest() {
+ // Modules
+ addons.add(Joke())
+ addons.add(RockPaperScissors())
+ addons.add(Twitter()) // no properties, disabled.
+ addons.add(War())
+ addons.add(Dice())
+ addons.add(Lookup())
+ assertThat(addons.modules.size, "modules = 2").isEqualTo(2)
+ assertThat(addons.names.modules, "module names").containsExactly("Joke", "RockPaperScissors")
+
+ // Commands
+ addons.add(View())
+ addons.add(Comment())
+ addons.add(Cycle())
+ addons.add(Die()) // invisible
+ addons.add(ChannelFeed("channel")) // no properties, disabled
+ p[Ignore.IGNORE_PROP] = "nick"
+ addons.add(Ignore())
+ assertThat(addons.commands.size, "commands = 3").isEqualTo(3)
+
+ assertThat(addons.names.ops, "ops names").containsExactly("cycle")
+
+ assertThat(addons.names.commands, "command names").containsExactly(
+ "joke",
+ "rock",
+ "paper",
+ "scissors",
+ "ignore"
+ )
+ }
+}
diff --git a/src/main/java/net/thauvin/erik/mobibot/msg/NoticeMessage.java b/src/test/kotlin/net/thauvin/erik/mobibot/ExceptionSanitizer.kt
similarity index 56%
rename from src/main/java/net/thauvin/erik/mobibot/msg/NoticeMessage.java
rename to src/test/kotlin/net/thauvin/erik/mobibot/ExceptionSanitizer.kt
index 258d947..ba486f0 100644
--- a/src/main/java/net/thauvin/erik/mobibot/msg/NoticeMessage.java
+++ b/src/test/kotlin/net/thauvin/erik/mobibot/ExceptionSanitizer.kt
@@ -1,7 +1,7 @@
/*
- * NoticeMessage.java
+ * ExceptionSanitizer.kt
*
- * Copyright (c) 2004-2019, Erik C. Thauvin (erik@thauvin.net)
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -30,37 +30,32 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package net.thauvin.erik.mobibot.msg;
+package net.thauvin.erik.mobibot
-/**
- * The NoticeMessage class.
- *
- * @author Erik C. Thauvin
- * @created 2019-04-07
- * @since 1.0
- */
-public class NoticeMessage extends Message {
- /**
- * Creates a new notice.
- *
- * @param message The notice's message.
- */
- public NoticeMessage(final String message) {
- super();
- this.setMessage(message);
- this.setNotice(true);
- }
+import net.thauvin.erik.mobibot.Utils.obfuscate
+import net.thauvin.erik.mobibot.Utils.replaceEach
+import net.thauvin.erik.mobibot.modules.ModuleException
+object ExceptionSanitizer {
/**
- * Create a new notice.
- *
- * @param message The notice's message.
- * @param color The color.
+ * Returns a sanitized exception to avoid displaying api keys, etc. in CI logs.
*/
- public NoticeMessage(final String message, final String color) {
- super();
- this.setMessage(message);
- this.setNotice(true);
- this.setColor(color);
+ fun ModuleException.sanitize(vararg sanitize: String): ModuleException {
+ val search = sanitize.filter { it.isNotBlank() }.toTypedArray()
+ if (search.isNotEmpty()) {
+ val obfuscate = search.map { it.obfuscate() }.toTypedArray()
+ with(this) {
+ if (!cause?.message.isNullOrBlank()) {
+ return ModuleException(
+ debugMessage,
+ cause!!.javaClass.name + ": " + cause!!.message!!.replaceEach(search, obfuscate),
+ this
+ )
+ } else if (!message.isNullOrBlank()) {
+ return ModuleException(debugMessage, message!!.replaceEach(search, obfuscate), this)
+ }
+ }
+ }
+ return this
}
}
diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/FeedReaderTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/FeedReaderTest.kt
new file mode 100644
index 0000000..e0e6aa3
--- /dev/null
+++ b/src/test/kotlin/net/thauvin/erik/mobibot/FeedReaderTest.kt
@@ -0,0 +1,73 @@
+/*
+ * FeedReaderTest.kt
+ *
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package net.thauvin.erik.mobibot
+
+import assertk.assertThat
+import assertk.assertions.contains
+import assertk.assertions.isEqualTo
+import assertk.assertions.isFailure
+import assertk.assertions.isInstanceOf
+import com.rometools.rome.io.FeedException
+import net.thauvin.erik.mobibot.FeedReader.Companion.readFeed
+import org.testng.annotations.Test
+import java.io.FileNotFoundException
+import java.net.MalformedURLException
+import java.net.UnknownHostException
+
+/**
+ * The `FeedReader Test` class.
+ */
+class FeedReaderTest {
+ @Test
+ fun readFeedTest() {
+ var messages = readFeed("https://feeds.thauvin.net/ethauvin")
+ assertThat(messages.size, "size = 10").isEqualTo(10)
+ assertThat(messages[1].msg, "feed entry url").contains("erik.thauvin.net")
+
+ messages = readFeed("https://lorem-rss.herokuapp.com/feed?length=0")
+ assertThat(messages[0].msg, "nothing to view").contains("nothing")
+
+ messages = readFeed("https://lorem-rss.herokuapp.com/feed?length=42", 42)
+ assertThat(messages.size, "messages = 84").isEqualTo(84)
+ assertThat(messages.last().msg, "example entry url").contains("http://example.com/test/")
+
+ assertThat { readFeed("blah") }.isFailure().isInstanceOf(MalformedURLException::class.java)
+
+ assertThat { readFeed("https://www.example.com") }.isFailure().isInstanceOf(FeedException::class.java)
+
+ assertThat { readFeed("https://www.examples.com/foo") }.isFailure()
+ .isInstanceOf(FileNotFoundException::class.java)
+
+ assertThat { readFeed("https://www.examplesfoo.com/") }.isFailure()
+ .isInstanceOf(UnknownHostException::class.java)
+ }
+}
diff --git a/src/test/java/net/thauvin/erik/mobibot/modules/LocalProperties.java b/src/test/kotlin/net/thauvin/erik/mobibot/LocalProperties.kt
similarity index 55%
rename from src/test/java/net/thauvin/erik/mobibot/modules/LocalProperties.java
rename to src/test/kotlin/net/thauvin/erik/mobibot/LocalProperties.kt
index 1a62f34..9249006 100644
--- a/src/test/java/net/thauvin/erik/mobibot/modules/LocalProperties.java
+++ b/src/test/kotlin/net/thauvin/erik/mobibot/LocalProperties.kt
@@ -1,7 +1,7 @@
/*
- * LocalProperties.java
+ * LocalProperties.kt
*
- * Copyright (c) 2004-2019, Erik C. Thauvin (erik@thauvin.net)
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -29,54 +29,47 @@
* 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
-package net.thauvin.erik.mobibot.modules;
-
-import net.thauvin.erik.mobibot.Constants;
-import org.testng.annotations.BeforeSuite;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.Properties;
+import org.testng.annotations.BeforeSuite
+import java.io.IOException
+import java.nio.file.Files
+import java.nio.file.Paths
+import java.util.Properties
/**
- * The properties class.
- *
- * @author Erik C. Thauvin
- * @created 2019-04-08
- * @since 1.0
+ * Access to `local.properties`.
*/
-class LocalProperties {
- private static final Properties localProps = new Properties();
-
- static String getProperty(final String key) {
- if (localProps.containsKey(key)) {
- return localProps.getProperty(key);
- } else {
- final String env = System.getenv(keyToEnv(key));
- if (env != null) {
- localProps.setProperty(key, env);
- }
- return env;
- }
- }
-
- private static String keyToEnv(final String key) {
- return key.replace('-', '_').toUpperCase(Constants.LOCALE);
- }
-
+open class LocalProperties {
@BeforeSuite(alwaysRun = true)
- public void loadProperties() {
- final Path localPath = Paths.get("local.properties");
+ fun loadProperties() {
+ val localPath = Paths.get("local.properties")
if (Files.exists(localPath)) {
- try (final InputStream stream = Files.newInputStream(localPath)) {
- localProps.load(stream);
- } catch (IOException ignore) {
+ try {
+ Files.newInputStream(localPath).use { stream -> localProps.load(stream) }
+ } catch (ignore: IOException) {
// Do nothing
}
}
}
+
+ companion object {
+ private val localProps = Properties()
+
+ fun getProperty(key: String): String {
+ return if (localProps.containsKey(key)) {
+ localProps.getProperty(key)
+ } else {
+ val env = System.getenv(keyToEnv(key))
+ env?.let {
+ localProps.setProperty(key, env)
+ }
+ env
+ }
+ }
+
+ private fun keyToEnv(key: String): String {
+ return key.replace('-', '_').uppercase()
+ }
+ }
}
diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/PinboardTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/PinboardTest.kt
new file mode 100644
index 0000000..70c6b28
--- /dev/null
+++ b/src/test/kotlin/net/thauvin/erik/mobibot/PinboardTest.kt
@@ -0,0 +1,84 @@
+/*
+ * PinboardTest.kt
+ *
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package net.thauvin.erik.mobibot
+
+import net.thauvin.erik.mobibot.entries.EntryLink
+import org.testng.Assert.assertFalse
+import org.testng.Assert.assertTrue
+import org.testng.annotations.Test
+import java.net.URL
+
+class PinboardTest : LocalProperties() {
+ private val pinboard = Pinboard()
+
+ @Test
+ fun testPinboard() {
+ val apiToken = getProperty("pinboard-api-token")
+ val url = "https://www.example.com/"
+ val ircServer = "irc.test.com"
+ val entry = EntryLink(url, "Test Example", "ErikT", "", "#mobitopia", listOf("test"))
+
+ pinboard.setApiToken(apiToken)
+
+ pinboard.addPin(ircServer, entry)
+ assertTrue(validatePin(apiToken, url = entry.link, entry.title, entry.nick, entry.channel), "validate add")
+
+ entry.link = "https://www.foo.com/"
+ pinboard.updatePin(ircServer, url, entry)
+ assertTrue(validatePin(apiToken, url = entry.link, ircServer), "validate update")
+
+ entry.title = "Foo Title"
+ pinboard.updatePin(ircServer, entry.link, entry)
+ assertTrue(validatePin(apiToken, url = entry.link, entry.title), "validate title")
+
+ pinboard.deletePin(entry)
+ assertFalse(validatePin(apiToken, url = entry.link), "validate delete")
+ }
+
+ private fun validatePin(apiToken: String, url: String, vararg matches: String): Boolean {
+ val response = Utils.urlReader(
+ URL(
+ "https://api.pinboard.in/v1/posts/get?auth_token=${apiToken}&tag=test&"
+ + Utils.encodeUrl(url)
+ )
+ )
+
+ matches.forEach {
+ if (!response.contains(it)) {
+ return false
+ }
+ }
+
+ return response.contains(url)
+ }
+}
diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/UtilsTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/UtilsTest.kt
new file mode 100644
index 0000000..06bb589
--- /dev/null
+++ b/src/test/kotlin/net/thauvin/erik/mobibot/UtilsTest.kt
@@ -0,0 +1,286 @@
+/*
+ * UtilsTest.kt
+ *
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package net.thauvin.erik.mobibot
+
+import assertk.all
+import assertk.assertThat
+import assertk.assertions.isEqualTo
+import assertk.assertions.length
+import net.thauvin.erik.mobibot.Utils.appendIfMissing
+import net.thauvin.erik.mobibot.Utils.bold
+import net.thauvin.erik.mobibot.Utils.buildCmdSyntax
+import net.thauvin.erik.mobibot.Utils.capitalise
+import net.thauvin.erik.mobibot.Utils.capitalizeWords
+import net.thauvin.erik.mobibot.Utils.colorize
+import net.thauvin.erik.mobibot.Utils.cyan
+import net.thauvin.erik.mobibot.Utils.encodeUrl
+import net.thauvin.erik.mobibot.Utils.getIntProperty
+import net.thauvin.erik.mobibot.Utils.green
+import net.thauvin.erik.mobibot.Utils.helpFormat
+import net.thauvin.erik.mobibot.Utils.lastOrEmpty
+import net.thauvin.erik.mobibot.Utils.obfuscate
+import net.thauvin.erik.mobibot.Utils.plural
+import net.thauvin.erik.mobibot.Utils.red
+import net.thauvin.erik.mobibot.Utils.replaceEach
+import net.thauvin.erik.mobibot.Utils.reverseColor
+import net.thauvin.erik.mobibot.Utils.toIntOrDefault
+import net.thauvin.erik.mobibot.Utils.toIsoLocalDate
+import net.thauvin.erik.mobibot.Utils.toUtcDateTime
+import net.thauvin.erik.mobibot.Utils.today
+import net.thauvin.erik.mobibot.Utils.unescapeXml
+import net.thauvin.erik.mobibot.Utils.uptime
+import net.thauvin.erik.mobibot.Utils.urlReader
+import net.thauvin.erik.mobibot.msg.Message.Companion.DEFAULT_COLOR
+import org.pircbotx.Colors
+import org.testng.annotations.BeforeClass
+import org.testng.annotations.Test
+import java.io.File
+import java.io.IOException
+import java.net.URL
+import java.time.LocalDateTime
+import java.util.Calendar
+import java.util.Properties
+
+/**
+ * The `Utils Test` class.
+ */
+class UtilsTest {
+ private val ascii =
+ " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"
+ private val cal = Calendar.getInstance()
+ private val localDateTime = LocalDateTime.of(1952, 2, 17, 12, 30, 0)
+ private val test = "This is a test."
+
+ @BeforeClass
+ fun setUp() {
+ cal[1952, Calendar.FEBRUARY, 17, 12, 30] = 0
+ }
+
+ @Test
+ fun testAppendIfMissing() {
+ val dir = "dir"
+ val sep = '/'
+ val url = "https://erik.thauvin.net"
+ assertThat(dir.appendIfMissing(File.separatorChar), "appendIfMissing(dir)")
+ .isEqualTo(dir + File.separatorChar)
+ assertThat(url.appendIfMissing(sep), "appendIfMissing(url)").isEqualTo("$url$sep")
+ assertThat("$url$sep".appendIfMissing(sep), "appendIfMissing($url$sep)").isEqualTo("$url$sep")
+ }
+
+ @Test
+ fun testBold() {
+ assertThat(1.bold(), "1.bold()").isEqualTo(Colors.BOLD + "1" + Colors.BOLD)
+ assertThat(2L.bold(), "1.bold()").isEqualTo(Colors.BOLD + "2" + Colors.BOLD)
+ assertThat(ascii.bold(), "ascii.bold()").isEqualTo(Colors.BOLD + ascii + Colors.BOLD)
+ assertThat("test".bold(), "test.bold()").isEqualTo(Colors.BOLD + "test" + Colors.BOLD)
+ }
+
+ @Test
+ fun testBuildCmdSyntax() {
+ val bot = "mobibot"
+ assertThat(buildCmdSyntax("%c $test %n $test", bot, false), "public")
+ .isEqualTo("$bot: $test $bot $test")
+ assertThat(buildCmdSyntax("%c %n $test %c $test %n", bot, true), "public")
+ .isEqualTo("/msg $bot $bot $test /msg $bot $test $bot")
+ }
+
+
+ @Test
+ fun testCapitalise() {
+ assertThat("test".capitalise(), "capitalize(test)").isEqualTo("Test")
+ assertThat("Test".capitalise(), "capitalize(Test)").isEqualTo("Test")
+ assertThat(test.capitalise(), "capitalize($test)").isEqualTo(test)
+ assertThat("".capitalise(), "capitalize()").isEqualTo("")
+ }
+
+ @Test
+ fun textCapitaliseWords() {
+ assertThat(test.capitalizeWords(), "captiatlizeWords(test)").isEqualTo("This Is A Test.")
+ assertThat("Already Capitalized".capitalizeWords(), "already capitalized")
+ .isEqualTo("Already Capitalized")
+ assertThat(" a test ".capitalizeWords(), "with spaces").isEqualTo(" A Test ")
+ }
+
+ @Test
+ fun testColorize() {
+ assertThat(ascii.colorize(Colors.REVERSE), "reverse.colorize()").isEqualTo(
+ Colors.REVERSE + ascii + Colors.REVERSE
+ )
+ assertThat(ascii.colorize(Colors.RED), "red.colorize()")
+ .isEqualTo(Colors.RED + ascii + Colors.NORMAL)
+ assertThat(ascii.colorize(Colors.BOLD), "colorized(bold)")
+ .isEqualTo(Colors.BOLD + ascii + Colors.BOLD)
+ assertThat(null.colorize(Colors.RED), "null.colorize()").isEqualTo("")
+ assertThat("".colorize(Colors.RED), "colorize()").isEqualTo("")
+ assertThat(ascii.colorize(DEFAULT_COLOR), "none.colorize()").isEqualTo(ascii)
+ assertThat(" ".colorize(Colors.NORMAL), "blank.colorize()")
+ .isEqualTo(Colors.NORMAL + " " + Colors.NORMAL)
+ }
+
+ @Test
+ fun testCyan() {
+ assertThat(ascii.cyan()).isEqualTo(Colors.CYAN + ascii + Colors.NORMAL)
+ }
+
+ @Test
+ fun testEncodeUrl() {
+ assertThat(encodeUrl("Hello Günter")).isEqualTo("Hello+G%C3%BCnter")
+ }
+
+ @Test
+ fun testGetIntProperty() {
+ val p = Properties()
+ p["one"] = "1"
+ p["two"] = "two"
+ assertThat(p.getIntProperty("one", 9), "getIntProperty(one)").isEqualTo(1)
+ assertThat(p.getIntProperty("two", 2), "getIntProperty(two)").isEqualTo(2)
+ assertThat(p.getIntProperty("foo", 3), "getIntProperty(foo)").isEqualTo(3)
+ }
+
+ @Test
+ fun testGreen() {
+ assertThat(ascii.green()).isEqualTo(Colors.DARK_GREEN + ascii + Colors.NORMAL)
+ }
+
+ @Test
+ fun testHelpFormat() {
+ assertThat(helpFormat(test, isBold = true, isIndent = false), "bold")
+ .isEqualTo("${Colors.BOLD}$test${Colors.BOLD}")
+ assertThat(helpFormat(test, isBold = false, isIndent = true), "indent")
+ .isEqualTo(test.prependIndent())
+ assertThat(helpFormat(test, isBold = true, isIndent = true), "bold-indent")
+ .isEqualTo(test.colorize(Colors.BOLD).prependIndent())
+
+ }
+
+ @Test
+ fun testIsoLocalDate() {
+ assertThat(cal.time.toIsoLocalDate(), "isoLocalDate(date)").isEqualTo("1952-02-17")
+ assertThat(localDateTime.toIsoLocalDate(), "isoLocalDate(localDate)").isEqualTo("1952-02-17")
+ }
+
+ @Test
+ fun testLastOrEmpty() {
+ val two = listOf("1", "2")
+ assertThat(two.lastOrEmpty(), "two").isEqualTo("2")
+ val one = listOf("1")
+ assertThat(one.lastOrEmpty(), "one").isEqualTo("")
+ }
+
+ @Test
+ fun testObfuscate() {
+ assertThat(ascii.obfuscate(), "obfuscate()").all {
+ length().isEqualTo(ascii.length)
+ isEqualTo(("x".repeat(ascii.length)))
+ }
+ assertThat(" ".obfuscate(), "obfuscate(blank)").isEqualTo(" ")
+ }
+
+ @Test
+ fun testPlural() {
+ val week = "week"
+ val weeks = "weeks"
+
+ for (i in -1..3) {
+ assertThat(week.plural(i.toLong()), "plural($i)").isEqualTo(if (i > 1) weeks else week)
+ }
+ }
+
+ @Test
+ fun testReplaceEach() {
+ val search = arrayOf("one", "two", "three")
+ val replace = arrayOf("1", "2", "3")
+ assertThat(search.joinToString(",").replaceEach(search, replace), "replaceEach(1,2,3")
+ .isEqualTo(replace.joinToString(","))
+
+ assertThat(test.replaceEach(search, replace), "replaceEach(nothing)").isEqualTo(test)
+
+ assertThat(test.replaceEach(arrayOf("t", "e"), arrayOf("", "E")), "replaceEach($test)")
+ .isEqualTo(test.replace("t", "").replace("e", "E"))
+
+ assertThat(test.replaceEach(search, emptyArray()), "replaceEach(search, empty)")
+ .isEqualTo(test)
+ }
+
+ @Test
+ fun testRed() {
+ assertThat(ascii.red()).isEqualTo(ascii.colorize(Colors.RED))
+ }
+
+ @Test
+ fun testReverseColor() {
+ assertThat(ascii.reverseColor()).isEqualTo(Colors.REVERSE + ascii + Colors.REVERSE)
+ }
+
+ @Test
+ fun testToday() {
+ assertThat(today()).isEqualTo(LocalDateTime.now().toIsoLocalDate())
+ }
+
+ @Test
+ fun testToIntOrDefault() {
+ assertThat("10".toIntOrDefault(1), "toIntOrDefault(10, 1)").isEqualTo(10)
+ assertThat("a".toIntOrDefault(2), "toIntOrDefault(a, 2)").isEqualTo(2)
+ }
+
+ @Test
+ fun testUnescapeXml() {
+ assertThat(unescapeXml("<a name="test & ''">")).isEqualTo(
+ ""
+ )
+ }
+
+ @Test
+ fun testUptime() {
+ assertThat(uptime(547800300076L), "full")
+ .isEqualTo("17 years 2 months 2 weeks 1 day 6 hours 45 minutes")
+ assertThat(uptime(2700000L), "minutes").isEqualTo("45 minutes")
+ assertThat(uptime(24300000L), "hours minutes").isEqualTo("6 hours 45 minutes")
+ assertThat(uptime(110700000L), "days hours minutes").isEqualTo("1 day 6 hours 45 minutes")
+ assertThat(uptime(1320300000L), "weeks days hours minutes")
+ .isEqualTo("2 weeks 1 day 6 hours 45 minutes")
+ assertThat(uptime(0L), "0 minutes").isEqualTo("0 minute")
+ }
+
+ @Test
+ @Throws(IOException::class)
+ fun testUrlReader() {
+ assertThat(urlReader(URL("https://postman-echo.com/status/200")), "urlReader()")
+ .isEqualTo("{\"status\":200}")
+ }
+
+ @Test
+ fun testUtcDateTime() {
+ assertThat(cal.time.toUtcDateTime(), "utcDateTime(date)").isEqualTo("1952-02-17 12:30")
+ assertThat(localDateTime.toUtcDateTime(), "utcDateTime(localDate)").isEqualTo("1952-02-17 12:30")
+ }
+}
diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/commands/RecapTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/commands/RecapTest.kt
new file mode 100644
index 0000000..ea3432e
--- /dev/null
+++ b/src/test/kotlin/net/thauvin/erik/mobibot/commands/RecapTest.kt
@@ -0,0 +1,61 @@
+/*
+ * RecapTest.kt
+ *
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package net.thauvin.erik.mobibot.commands
+
+import assertk.all
+import assertk.assertThat
+import assertk.assertions.isEqualTo
+import assertk.assertions.matches
+import assertk.assertions.prop
+import assertk.assertions.size
+import org.testng.annotations.Test
+
+class RecapTest {
+ @Test
+ fun storeRecapTest() {
+ for (i in 1..20) {
+ Recap.storeRecap("sender$i", "test $i", false)
+ }
+ assertThat(Recap.recaps, "recap first and last").all {
+ size().isEqualTo(Recap.MAX_RECAPS)
+ prop(MutableList::first)
+ .matches("[1-2]\\d{3}-[01]\\d-[0-3]\\d [0-2]\\d:[0-6]\\d - sender11: test 11".toRegex())
+ prop(MutableList::last)
+ .matches("[1-2]\\d{3}-[01]\\d-[0-3]\\d [0-2]\\d:[0-6]\\d - sender20: test 20".toRegex())
+ }
+
+ Recap.storeRecap("sender", "test action", true)
+ assertThat(Recap.recaps.last())
+ .matches("[1-2]\\d{3}-[01]\\d-[0-3]\\d [0-2]\\d:[0-6]\\d - sender test action".toRegex())
+ }
+}
diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/commands/links/LinksMgrTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/commands/links/LinksMgrTest.kt
new file mode 100644
index 0000000..551ee1d
--- /dev/null
+++ b/src/test/kotlin/net/thauvin/erik/mobibot/commands/links/LinksMgrTest.kt
@@ -0,0 +1,75 @@
+/*
+ * LinksMgrTest.kt
+ *
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package net.thauvin.erik.mobibot.commands.links
+
+import assertk.all
+import assertk.assertThat
+import assertk.assertions.contains
+import assertk.assertions.isEqualTo
+import assertk.assertions.isTrue
+import assertk.assertions.size
+import net.thauvin.erik.mobibot.Constants
+import org.testng.annotations.Test
+
+class LinksMgrTest {
+ private val linksMgr = LinksMgr()
+
+ @Test
+ fun fetchTitle() {
+ assertThat(linksMgr.fetchTitle("https://erik.thauvin.net/"), "Erik").contains("Erik's Weblog")
+ assertThat(linksMgr.fetchTitle("https://www.google.com/foo"), "Foo").isEqualTo(Constants.NO_TITLE)
+ }
+
+ @Test
+ fun testMatches() {
+ assertThat(linksMgr.matches("https://www.example.com/"), "https").isTrue()
+ assertThat(linksMgr.matches("HTTP://erik.thauvin.net/blog/ Erik's Weblog"), "HTTP").isTrue()
+ }
+
+ @Test
+ fun matchTagKeywordsTest() {
+ linksMgr.setProperty(LinksMgr.KEYWORDS_PROP, "key1 key2,key3")
+ val tags = mutableListOf()
+
+ linksMgr.matchTagKeywords("Test title with key2", tags)
+ assertThat(tags, "key2").contains("key2")
+ tags.clear()
+
+ linksMgr.matchTagKeywords("Test key3 title with key1", tags)
+ assertThat(tags, "key1 & key 3").all {
+ contains("key1")
+ contains("key3")
+ size().isEqualTo(2)
+ }
+ }
+}
diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/commands/links/ViewTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/commands/links/ViewTest.kt
new file mode 100644
index 0000000..f5f30be
--- /dev/null
+++ b/src/test/kotlin/net/thauvin/erik/mobibot/commands/links/ViewTest.kt
@@ -0,0 +1,112 @@
+/*
+ * ViewTest.kt
+ *
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package net.thauvin.erik.mobibot.commands.links
+
+import assertk.all
+import assertk.assertThat
+import assertk.assertions.isEqualTo
+import assertk.assertions.prop
+import net.thauvin.erik.mobibot.entries.EntryLink
+import org.testng.annotations.Test
+
+class ViewTest {
+ @Test
+ fun testParseArgs() {
+ val view = View()
+
+ for (i in 1..10) {
+ LinksMgr.entries.links.add(
+ EntryLink(
+ "https://www.example.com/$i",
+ "Example $i",
+ "nick$i",
+ "login$i",
+ "#channel",
+ emptyList()
+ )
+ )
+ }
+
+ assertThat(view.parseArgs("1"), "parseArgs(1)").all {
+ prop(Pair::first).isEqualTo(0)
+ prop(Pair::second).isEqualTo("")
+ }
+
+ assertThat(view.parseArgs("2 foo"), "parseArgs(2, foo)").all {
+ prop(Pair::first).isEqualTo(1)
+ prop(Pair::second).isEqualTo("foo")
+ }
+
+ assertThat(view.parseArgs("3 FOO"), "parseArgs(3, FOO)").all {
+ prop(Pair::first).isEqualTo(2)
+ prop(Pair::second).isEqualTo("foo")
+ }
+
+ assertThat(view.parseArgs(" 4 foo bar "), "parseArgs( 4 foo bar )").all {
+ prop(Pair::first).isEqualTo(3)
+ prop(Pair::second).isEqualTo("foo bar")
+ }
+
+ assertThat(view.parseArgs("foo bar"), "parseArgs(foo bar)").all {
+ prop(Pair::first).isEqualTo(0)
+ prop(Pair::second).isEqualTo("foo bar")
+ }
+
+ assertThat(view.parseArgs("${Int.MAX_VALUE}1"), "parseArgs(overflow)").all {
+ prop(Pair::first).isEqualTo(0)
+ prop(Pair::second).isEqualTo("${Int.MAX_VALUE}1")
+ }
+
+ assertThat(view.parseArgs("1a"), "parseArgs(1a)").all {
+ prop(Pair::first).isEqualTo(0)
+ prop(Pair::second).isEqualTo("1a")
+ }
+
+ assertThat(view.parseArgs("20"), "parseArgs(20)").all {
+ prop(Pair::first).isEqualTo(0)
+ prop(Pair::second).isEqualTo("")
+ }
+
+ assertThat(view.parseArgs(""), "parseArgs()").all {
+ prop(Pair::first).isEqualTo(LinksMgr.entries.links.size - View.MAX_ENTRIES)
+ prop(Pair::second).isEqualTo("")
+ }
+
+ LinksMgr.entries.links.clear()
+
+ assertThat(view.parseArgs("4"), "parseArgs(4)").all {
+ prop(Pair::first).isEqualTo(0)
+ prop(Pair::second).isEqualTo("")
+ }
+ }
+}
diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/commands/tell/TellMessageTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/commands/tell/TellMessageTest.kt
new file mode 100644
index 0000000..436b81d
--- /dev/null
+++ b/src/test/kotlin/net/thauvin/erik/mobibot/commands/tell/TellMessageTest.kt
@@ -0,0 +1,73 @@
+/*
+ * TellMessageTest.kt
+ *
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package net.thauvin.erik.mobibot.commands.tell
+
+import assertk.all
+import assertk.assertThat
+import assertk.assertions.isEqualTo
+import assertk.assertions.isFalse
+import assertk.assertions.isTrue
+import assertk.assertions.prop
+import org.testng.annotations.Test
+import java.time.Duration
+import java.time.LocalDateTime
+import java.time.temporal.Temporal
+
+/**
+ * The `TellMessageTest` class.
+ */
+class TellMessageTest {
+ private fun isValidDate(date: Temporal): Boolean {
+ return Duration.between(date, LocalDateTime.now()).toMinutes() < 1
+ }
+
+ @Test
+ fun testTellMessage() {
+ val message = "Test message."
+ val recipient = "recipient"
+ val sender = "sender"
+ val tellMessage = TellMessage(sender, recipient, message)
+ assertThat(tellMessage).all {
+ prop(TellMessage::sender).isEqualTo(sender)
+ prop(TellMessage::recipient).isEqualTo(recipient)
+ prop(TellMessage::message).isEqualTo(message)
+ }
+ assertThat(isValidDate(tellMessage.queued), "queued is valid date/time").isTrue()
+ assertThat(tellMessage.isMatch(sender), "match sender").isTrue()
+ assertThat(tellMessage.isMatch(recipient), "match recipient").isTrue()
+ assertThat(tellMessage.isMatch("foo"), "foo is no match").isFalse()
+ tellMessage.isReceived = false
+ assertThat(tellMessage.receptionDate, "reception date not set").isEqualTo(LocalDateTime.MIN)
+ tellMessage.isReceived = true
+ assertThat(isValidDate(tellMessage.receptionDate), "received is valid date/time").isTrue()
+ }
+}
diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/commands/tell/TellMessagesMgrTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/commands/tell/TellMessagesMgrTest.kt
new file mode 100644
index 0000000..a2563c3
--- /dev/null
+++ b/src/test/kotlin/net/thauvin/erik/mobibot/commands/tell/TellMessagesMgrTest.kt
@@ -0,0 +1,93 @@
+/*
+ * TellMessagesMgrTest.kt
+ *
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package net.thauvin.erik.mobibot.commands.tell
+
+import assertk.all
+import assertk.assertThat
+import assertk.assertions.index
+import assertk.assertions.isEqualTo
+import assertk.assertions.isFalse
+import assertk.assertions.isGreaterThan
+import assertk.assertions.isTrue
+import assertk.assertions.prop
+import org.testng.annotations.AfterClass
+import org.testng.annotations.BeforeClass
+import org.testng.annotations.Test
+import java.time.LocalDateTime
+import kotlin.io.path.createTempFile
+import kotlin.io.path.deleteIfExists
+import kotlin.io.path.fileSize
+
+class TellMessagesMgrTest {
+ private val testFile = createTempFile(suffix = ".ser")
+ private val maxDays = 10L
+ private val testMessages = mutableListOf().apply {
+ for (i in 0..5) {
+ this.add(i, TellMessage("sender$i", "recipient$i", "message $i"))
+ }
+ }
+
+ @BeforeClass
+ fun saveTest() {
+ TellMessagesMgr.save(testFile.toAbsolutePath().toString(), testMessages)
+ assertThat(testFile.fileSize()).isGreaterThan(0)
+ }
+
+ @AfterClass
+ fun afterClass() {
+ testFile.deleteIfExists()
+ }
+
+ @Test
+ fun cleanTest() {
+ testMessages.add(TellMessage("sender", "recipient", "message").apply {
+ queued = LocalDateTime.now().minusDays(maxDays)
+ })
+ val size = testMessages.size
+ assertThat(TellMessagesMgr.clean(testMessages, maxDays + 2), "maxDays = 12").isFalse()
+ assertThat(TellMessagesMgr.clean(testMessages, maxDays), "maxDays = 10").isTrue()
+ assertThat(testMessages.size, "size-- after clean").isEqualTo(size - 1)
+ }
+
+ @Test
+ fun loadTest() {
+ val messages = TellMessagesMgr.load(testFile.toAbsolutePath().toString())
+ for (i in messages.indices) {
+ assertThat(messages).index(i).all {
+ prop(TellMessage::sender).isEqualTo(testMessages[i].sender)
+ prop(TellMessage::recipient).isEqualTo(testMessages[i].recipient)
+ prop(TellMessage::message).isEqualTo(testMessages[i].message)
+ }
+ }
+ }
+}
diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/entries/EntriesUtilsTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/entries/EntriesUtilsTest.kt
new file mode 100644
index 0000000..3b39be4
--- /dev/null
+++ b/src/test/kotlin/net/thauvin/erik/mobibot/entries/EntriesUtilsTest.kt
@@ -0,0 +1,92 @@
+/*
+ * EntriesUtilsTest.kt
+ *
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package net.thauvin.erik.mobibot.entries
+
+import assertk.assertThat
+import assertk.assertions.contains
+import assertk.assertions.isEqualTo
+import net.thauvin.erik.mobibot.Constants
+import net.thauvin.erik.mobibot.entries.EntriesUtils.buildComment
+import net.thauvin.erik.mobibot.entries.EntriesUtils.buildLink
+import net.thauvin.erik.mobibot.entries.EntriesUtils.buildLinkLabel
+import net.thauvin.erik.mobibot.entries.EntriesUtils.buildTags
+import org.testng.annotations.Test
+
+class EntriesUtilsTest {
+ private val comment = EntryComment("comment", "nick")
+ private val links = buildList {
+ for (i in 0..5) {
+ add(
+ EntryLink(
+ "https://www.mobitopia.org/$i",
+ "Mobitopia$i",
+ "Skynx$i",
+ "JimH$i",
+ "#mobitopia$i",
+ listOf("tag1", "tag2", "tag3", "TAG4", "Tag5")
+ )
+ )
+ }
+ }
+
+ @Test
+ fun buildLinkLabelTest() {
+ assertThat(buildLinkLabel(1)).isEqualTo("${Constants.LINK_CMD}2")
+ }
+
+ @Test
+ fun buildCommentTest() {
+ assertThat(buildComment(0, 0, comment)).isEqualTo("${Constants.LINK_CMD}1.1: [nick] comment")
+ }
+
+ @Test
+ fun buildLinkTest() {
+ for (i in links.indices) {
+ assertThat(
+ buildLink(i - 1, links[i]), "link $i"
+ ).isEqualTo("L$i: [Skynx$i] \u0002Mobitopia$i\u0002 ( \u000303https://www.mobitopia.org/$i\u000F )")
+ }
+
+ assertThat(links.first().addComment(comment), "add comment").isEqualTo(0)
+ assertThat(buildLink(0, links.first(), isView = true), "isView = true").contains("[+1]")
+ }
+
+ @Test
+ fun buildTagsTest() {
+ for (i in links.indices) {
+ assertThat(
+ buildTags(i - 1, links[i]), "tag $i"
+ ).isEqualTo("L${i}T: Skynx$i, tag1, tag2, tag3, tag4, tag5")
+ }
+ }
+}
diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/entries/EntryLinkTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/entries/EntryLinkTest.kt
new file mode 100644
index 0000000..36244de
--- /dev/null
+++ b/src/test/kotlin/net/thauvin/erik/mobibot/entries/EntryLinkTest.kt
@@ -0,0 +1,134 @@
+/*
+ * EntryLinkTest.kt
+ *
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package net.thauvin.erik.mobibot.entries
+
+import assertk.all
+import assertk.assertThat
+import assertk.assertions.isEmpty
+import assertk.assertions.isEqualTo
+import assertk.assertions.isFalse
+import assertk.assertions.isTrue
+import assertk.assertions.prop
+import assertk.assertions.size
+import com.rometools.rome.feed.synd.SyndCategory
+import com.rometools.rome.feed.synd.SyndCategoryImpl
+import org.testng.annotations.Test
+import java.security.SecureRandom
+import java.util.Date
+
+/**
+ * The `EntryUtilsTest` class.
+ *
+ * @author [Erik C. Thauvin](https://erik.thauvin.net/)
+ * @created 2019-04-19
+ * @since 1.0
+ */
+class EntryLinkTest {
+ private val entryLink = EntryLink(
+ "https://www.mobitopia.org/", "Mobitopia", "Skynx", "JimH", "#mobitopia",
+ listOf("tag1", "tag2", "tag3", "TAG4", "Tag5")
+ )
+
+ @Test
+ fun testAddDeleteComment() {
+ var i = 0
+ while (i < 5) {
+ entryLink.addComment("c$i", "u$i")
+ i++
+ }
+ assertThat(entryLink.comments.size, "getComments().size() == 5").isEqualTo(i)
+ i = 0
+ for (comment in entryLink.comments) {
+ assertThat(comment).all {
+ prop(EntryComment::comment).isEqualTo("c$i")
+ prop(EntryComment::nick).isEqualTo("u$i")
+ }
+ i++
+ }
+
+ val r = SecureRandom()
+ while (entryLink.comments.size > 0) {
+ entryLink.deleteComment(r.nextInt(entryLink.comments.size))
+ }
+ assertThat(entryLink.comments, "hasComments()").isEmpty()
+ entryLink.addComment("nothing", "nobody")
+ entryLink.setComment(0, "something", "somebody")
+ val comment = entryLink.getComment(0)
+ assertThat(comment, "get first comment").all {
+ prop(EntryComment::nick).isEqualTo("somebody")
+ prop(EntryComment::comment).isEqualTo("something")
+ }
+ assertThat(entryLink.deleteComment(comment), "delete comment").isTrue()
+ assertThat(entryLink.deleteComment(comment), "comment is already deleted").isFalse()
+ }
+
+ @Test
+ fun testConstructor() {
+ val tag = "test"
+ val tags = listOf(SyndCategoryImpl().apply { name = tag })
+ val link = EntryLink("link", "title", "nick", "channel", Date(), tags)
+ assertThat(link.tags.size, "check tag size").isEqualTo(tags.size)
+ assertThat(link.tags[0].name, "check tag name").isEqualTo(tag)
+ assertThat(link.pinboardTags, "check pinboard tags").isEqualTo("nick,$tag")
+ }
+
+ @Test
+ fun testMatches() {
+ assertThat(entryLink.matches("mobitopia"), "match mobitopia").isTrue()
+ assertThat(entryLink.matches("skynx"), "match nick").isTrue()
+ assertThat(entryLink.matches("www.mobitopia.org"), "match url").isTrue()
+ assertThat(entryLink.matches("foo"), "match foo").isFalse()
+ assertThat(entryLink.matches(""), "match empty").isFalse()
+ assertThat(entryLink.matches(null), "match null").isFalse()
+ }
+
+
+ @Test
+ fun testTags() {
+ val tags: List = entryLink.tags
+ for ((i, tag) in tags.withIndex()) {
+ assertThat(tag.name, "tag.getName($i)").isEqualTo("tag" + (i + 1))
+ }
+ assertThat(entryLink.tags, "size is 5").size().isEqualTo(5)
+ entryLink.setTags("-tag5")
+ entryLink.setTags("+mobitopia")
+ entryLink.setTags("tag4")
+ entryLink.setTags("-mobitopia")
+ assertThat(entryLink.pinboardTags, "getPinboardTags()")
+ .isEqualTo(entryLink.nick + ",tag1,tag2,tag3,tag4,mobitopia")
+ val size = entryLink.tags.size
+ entryLink.setTags("")
+ assertThat(entryLink.tags.size, "empty tag").isEqualTo(size)
+ entryLink.setTags(" ")
+ assertThat(entryLink.tags.size, "blank tag").isEqualTo(size)
+ }
+}
diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/entries/FeedMgrTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/entries/FeedMgrTest.kt
new file mode 100644
index 0000000..da0877d
--- /dev/null
+++ b/src/test/kotlin/net/thauvin/erik/mobibot/entries/FeedMgrTest.kt
@@ -0,0 +1,121 @@
+/*
+ * FeedMgrTest.kt
+ *
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package net.thauvin.erik.mobibot.entries
+
+import assertk.all
+import assertk.assertThat
+import assertk.assertions.endsWith
+import assertk.assertions.isEqualTo
+import assertk.assertions.isTrue
+import assertk.assertions.prop
+import net.thauvin.erik.mobibot.Utils.today
+import org.testng.annotations.BeforeSuite
+import org.testng.annotations.Test
+import java.nio.file.Paths
+import java.util.Date
+import kotlin.io.path.absolutePathString
+import kotlin.io.path.deleteIfExists
+import kotlin.io.path.exists
+import kotlin.io.path.fileSize
+import kotlin.io.path.name
+
+class FeedMgrTest {
+ private val entries = Entries()
+ private val channel = "mobibot"
+
+ @BeforeSuite(alwaysRun = true)
+ fun beforeSuite() {
+ entries.logsDir = "src/test/resources/"
+ entries.ircServer = "irc.example.com"
+ entries.channel = channel
+ entries.backlogs = "https://www.mobitopia.org/mobibot/logs"
+ }
+
+ @Test
+ fun testFeedMgr() {
+ // Load the feed
+ assertThat(FeedsMgr.loadFeed(entries), "pubDate").isEqualTo("2021-10-31")
+
+ assertThat(entries.links.size, "2 links").isEqualTo(2)
+ entries.links.forEachIndexed { i, entryLink ->
+ assertThat(entryLink, "Example $(i + 1)").all {
+ prop(EntryLink::title).isEqualTo("Example ${i + 1}")
+ prop(EntryLink::link).isEqualTo("https://www.example.com/${i + 1}")
+ prop(EntryLink::channel).isEqualTo(channel)
+ }
+ entryLink.tags.forEachIndexed { y, tag ->
+ assertThat(tag.name, "tag${i + 1}-${y + 1}").isEqualTo("tag${i + 1}-${y + 1}")
+ }
+ }
+
+ with(entries.links.first()) {
+ assertThat(nick, "first nick").isEqualTo("ErikT")
+ assertThat(date, "first date").isEqualTo(Date(1635638400000L))
+ assertThat(comments.first(), "first comment").all {
+ prop(EntryComment::comment).endsWith("comment 1.")
+ prop(EntryComment::nick).isEqualTo("ErikT")
+ }
+ assertThat(comments.last(), "last comment").all {
+ prop(EntryComment::comment).endsWith("comment 2.")
+ prop(EntryComment::nick).isEqualTo("Skynx")
+ }
+ }
+
+ assertThat(entries.links[1], "second link").all {
+ prop(EntryLink::nick).isEqualTo("Skynx")
+ prop(EntryLink::date).isEqualTo(Date(1635638460000L))
+ }
+
+ val currentFile = Paths.get("${entries.logsDir}test.xml")
+ val backlogFile = Paths.get("${entries.logsDir}${today()}.xml")
+
+ // Save the feed
+ FeedsMgr.saveFeed(entries, currentFile.name)
+
+ assertThat(currentFile.exists(), "${currentFile.absolutePathString()} exists").isTrue()
+ assertThat(backlogFile.exists(), "${backlogFile.absolutePathString()} exits").isTrue()
+
+ assertThat(currentFile.fileSize(), "files are identical").isEqualTo(backlogFile.fileSize())
+
+ // Load the test feed
+ entries.links.clear()
+ FeedsMgr.loadFeed(entries, currentFile.name)
+
+ entries.links.forEachIndexed { i, entryLink ->
+ assertThat(entryLink.title, "${currentFile.name} title ${i + 1}").isEqualTo("Example ${i + 1}")
+ }
+
+ assertThat(currentFile.deleteIfExists(), "delete ${currentFile.absolutePathString()}").isTrue()
+ assertThat(backlogFile.deleteIfExists(), "delete ${backlogFile.absolutePathString()}").isTrue()
+ }
+}
diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/modules/CalcTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/modules/CalcTest.kt
new file mode 100644
index 0000000..3d125f8
--- /dev/null
+++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/CalcTest.kt
@@ -0,0 +1,54 @@
+/*
+ * CalcTest.kt
+ *
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package net.thauvin.erik.mobibot.modules
+
+import assertk.assertThat
+import assertk.assertions.isEqualTo
+import assertk.assertions.isFailure
+import assertk.assertions.isInstanceOf
+import net.objecthunter.exp4j.tokenizer.UnknownFunctionOrVariableException
+import net.thauvin.erik.mobibot.Utils.bold
+import net.thauvin.erik.mobibot.modules.Calc.Companion.calculate
+import org.testng.annotations.Test
+
+/**
+ * The `CalcTest` class.
+ */
+class CalcTest {
+ @Test
+ fun testCalculate() {
+ assertThat(calculate("1 + 1"), "calculate(1+1)").isEqualTo("1+1 = ${2.bold()}")
+ assertThat(calculate("1 -3"), "calculate(1 -3)").isEqualTo("1-3 = ${(-2).bold()}")
+ assertThat(calculate("pi+π+e+φ"), "calculate(pi+π+e+φ)").isEqualTo("pi+π+e+φ = ${"10.62".bold()}")
+ assertThat { calculate("one + one") }.isFailure().isInstanceOf(UnknownFunctionOrVariableException::class.java)
+ }
+}
diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/modules/CryptoPricesTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/modules/CryptoPricesTest.kt
new file mode 100644
index 0000000..ca676d3
--- /dev/null
+++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/CryptoPricesTest.kt
@@ -0,0 +1,64 @@
+/*
+ * CryptoPricesTest.kt
+ *
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package net.thauvin.erik.mobibot.modules
+
+import assertk.all
+import assertk.assertThat
+import assertk.assertions.isEqualTo
+import assertk.assertions.isGreaterThan
+import assertk.assertions.prop
+import net.thauvin.erik.crypto.CryptoPrice
+import net.thauvin.erik.mobibot.modules.CryptoPrices.Companion.currentPrice
+import org.testng.annotations.Test
+
+/**
+ * The `CryptoPricesTest` class.
+ */
+class CryptoPricesTest {
+ @Test
+ @Throws(ModuleException::class)
+ fun testMarketPrice() {
+ var price = currentPrice(listOf("BTC"))
+ assertThat(price, "BTC in USD").all {
+ prop(CryptoPrice::base).isEqualTo("BTC")
+ prop(CryptoPrice::currency).isEqualTo("USD")
+ prop(CryptoPrice::amount).transform { it.signum() }.isGreaterThan(0)
+ }
+
+ price = currentPrice(listOf("ETH", "EUR"))
+ assertThat(price, "ETH in EUR").all {
+ prop(CryptoPrice::base).isEqualTo("ETH")
+ prop(CryptoPrice::currency).isEqualTo("EUR")
+ prop(CryptoPrice::amount).transform { it.signum() }.isGreaterThan(0)
+ }
+ }
+}
diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/modules/CurrencyConverterTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/modules/CurrencyConverterTest.kt
new file mode 100644
index 0000000..8ba5c3b
--- /dev/null
+++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/CurrencyConverterTest.kt
@@ -0,0 +1,88 @@
+/*
+ * CurrencyConverterTest.kt
+ *
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package net.thauvin.erik.mobibot.modules
+
+import assertk.all
+import assertk.assertThat
+import assertk.assertions.any
+import assertk.assertions.contains
+import assertk.assertions.isEqualTo
+import assertk.assertions.isInstanceOf
+import assertk.assertions.matches
+import assertk.assertions.prop
+import assertk.assertions.size
+import net.thauvin.erik.mobibot.modules.CurrencyConverter.Companion.convertCurrency
+import net.thauvin.erik.mobibot.modules.CurrencyConverter.Companion.currencyRates
+import net.thauvin.erik.mobibot.modules.CurrencyConverter.Companion.loadRates
+import net.thauvin.erik.mobibot.msg.ErrorMessage
+import net.thauvin.erik.mobibot.msg.Message
+import net.thauvin.erik.mobibot.msg.PublicMessage
+import org.testng.annotations.BeforeClass
+import org.testng.annotations.Test
+
+/**
+ * The `CurrencyConvertTest` class.
+ */
+class CurrencyConverterTest {
+ @BeforeClass
+ @Throws(ModuleException::class)
+ fun before() {
+ loadRates()
+ }
+
+ @Test
+ fun testConvertCurrency() {
+ assertThat(
+ convertCurrency("100 USD to EUR").msg,
+ "100 USD to EUR"
+ ).matches("\\$100\\.00 = €\\d{2,3}\\.\\d{2}".toRegex())
+ assertThat(convertCurrency("100 USD to USD"), "100 USD to USD").all {
+ prop(Message::msg).contains("You're kidding, right?")
+ isInstanceOf(PublicMessage::class.java)
+ }
+ assertThat(convertCurrency("100 USD"), "100 USD").all {
+ prop(Message::msg).contains("Invalid query.")
+ isInstanceOf(ErrorMessage::class.java)
+ }
+ }
+
+ @Test
+ fun testCurrencyRates() {
+ val rates = currencyRates()
+ assertThat(rates).all {
+ size().isEqualTo(33)
+ any { it.matches("[A-Z]{3}: +[\\d.]+".toRegex()) }
+ contains("EUR: 1")
+ }
+
+ }
+}
diff --git a/src/test/java/net/thauvin/erik/mobibot/modules/JokeTest.java b/src/test/kotlin/net/thauvin/erik/mobibot/modules/DiceTest.kt
similarity index 66%
rename from src/test/java/net/thauvin/erik/mobibot/modules/JokeTest.java
rename to src/test/kotlin/net/thauvin/erik/mobibot/modules/DiceTest.kt
index d62cd2e..a4a84c8 100644
--- a/src/test/java/net/thauvin/erik/mobibot/modules/JokeTest.java
+++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/DiceTest.kt
@@ -1,7 +1,7 @@
/*
- * JokeTest.java
+ * DiceTest.kt
*
- * Copyright (c) 2004-2019, Erik C. Thauvin (erik@thauvin.net)
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -30,28 +30,18 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package net.thauvin.erik.mobibot.modules;
+package net.thauvin.erik.mobibot.modules
-import org.testng.annotations.Test;
-import static org.assertj.core.api.Assertions.assertThat;
+import assertk.assertThat
+import assertk.assertions.isEqualTo
+import org.testng.annotations.Test
-/**
- * The JokeTest class.
- *
- * @author Erik C. Thauvin
- * @created 2019-04-07
- * @since 1.0
- */
-public class JokeTest {
+class DiceTest {
@Test
- public void testJokeImpl() {
- AbstractModuleTest.testAbstractModule(new Joke());
- }
-
- @Test
- public void testRamdomJoke() throws ModuleException {
- assertThat(Joke.randomJoke().getMessage().length() > 0).as("randomJoke() > 0").isTrue();
- assertThat(Joke.randomJoke().getMessage()).as("randomJoke()").containsIgnoringCase("chuck");
+ fun testWinLoseOrTie() {
+ assertThat(Dice.winLoseOrTie(6, 6), "6 vs. 6").isEqualTo(Dice.Result.TIE)
+ assertThat(Dice.winLoseOrTie(6, 5), "6 vs. 5").isEqualTo(Dice.Result.WIN)
+ assertThat(Dice.winLoseOrTie(5, 6), "5 vs. 6").isEqualTo(Dice.Result.LOSE)
}
}
diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/modules/GoogleSearchTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/modules/GoogleSearchTest.kt
new file mode 100644
index 0000000..003e825
--- /dev/null
+++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/GoogleSearchTest.kt
@@ -0,0 +1,93 @@
+/*
+ * GoogleSearchTest.kt
+ *
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package net.thauvin.erik.mobibot.modules
+
+import assertk.all
+import assertk.assertThat
+import assertk.assertions.contains
+import assertk.assertions.hasNoCause
+import assertk.assertions.isEqualTo
+import assertk.assertions.isFailure
+import assertk.assertions.isInstanceOf
+import assertk.assertions.isNotEmpty
+import assertk.assertions.prop
+import net.thauvin.erik.mobibot.ExceptionSanitizer.sanitize
+import net.thauvin.erik.mobibot.LocalProperties
+import net.thauvin.erik.mobibot.modules.GoogleSearch.Companion.searchGoogle
+import net.thauvin.erik.mobibot.msg.ErrorMessage
+import net.thauvin.erik.mobibot.msg.Message
+import org.testng.annotations.Test
+
+/**
+ * The `GoogleSearchTest` class.
+ */
+class GoogleSearchTest : LocalProperties() {
+ @Test
+ @Throws(ModuleException::class)
+ fun testSearchGoogle() {
+ val apiKey = getProperty(GoogleSearch.GOOGLE_API_KEY_PROP)
+ val cseKey = getProperty(GoogleSearch.GOOGLE_CSE_KEY_PROP)
+ try {
+ var messages = searchGoogle("mobitopia", apiKey, cseKey)
+ assertThat(messages, "mobitopia results not empty").isNotEmpty()
+ assertThat(messages[0].msg, "found mobibtopia").contains("mobitopia", true)
+
+ messages = searchGoogle("aapl", apiKey, cseKey)
+ assertThat(messages, "aapl results not empty").isNotEmpty()
+ assertThat(messages[0].msg, "found apple").contains("apple", true)
+
+ messages = searchGoogle("adadflkjl", apiKey, cseKey)
+ assertThat(messages[0], "not found").all {
+ isInstanceOf(ErrorMessage::class.java)
+ prop(Message::msg).isEqualTo("No results found.")
+ }
+
+ assertThat(
+ searchGoogle("", "apikey", "cssKey").first(),
+ "empty query"
+ ).isInstanceOf(ErrorMessage::class.java)
+
+ assertThat { searchGoogle("test", "", "apiKey") }.isFailure()
+ .isInstanceOf(ModuleException::class.java).hasNoCause()
+
+ assertThat { searchGoogle("test", "apiKey", "") }.isFailure()
+ .isInstanceOf(ModuleException::class.java).hasNoCause()
+ } catch (e: ModuleException) {
+ // Avoid displaying api keys in CI logs
+ if ("true" == System.getenv("CI")) {
+ throw e.sanitize(apiKey, cseKey)
+ } else {
+ throw e
+ }
+ }
+ }
+}
diff --git a/src/main/java/net/thauvin/erik/mobibot/msg/PrivateMessage.java b/src/test/kotlin/net/thauvin/erik/mobibot/modules/JokeTest.kt
similarity index 67%
rename from src/main/java/net/thauvin/erik/mobibot/msg/PrivateMessage.java
rename to src/test/kotlin/net/thauvin/erik/mobibot/modules/JokeTest.kt
index 1079a4d..65c259b 100644
--- a/src/main/java/net/thauvin/erik/mobibot/msg/PrivateMessage.java
+++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/JokeTest.kt
@@ -1,7 +1,7 @@
/*
- * PrivateMessage.java
+ * JokeTest.kt
*
- * Copyright (c) 2004-2019, Erik C. Thauvin (erik@thauvin.net)
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -29,32 +29,25 @@
* 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.modules
-package net.thauvin.erik.mobibot.msg;
+import assertk.all
+import assertk.assertThat
+import assertk.assertions.contains
+import assertk.assertions.isNotEmpty
+import net.thauvin.erik.mobibot.modules.Joke.Companion.randomJoke
+import org.testng.annotations.Test
/**
- * The PrivateMessage class.
- *
- * @author Erik C. Thauvin
- * @created 2019-04-09
- * @since 1.0
+ * The `JokeTest` class.
*/
-@SuppressWarnings("unused")
-public class PrivateMessage extends Message {
- public PrivateMessage(final String message) {
- super();
- this.setMessage(message);
- }
-
- /**
- * Creates a new private message.
- *
- * @param message The message.
- * @param color The message color.
- */
- public PrivateMessage(final String message, final String color) {
- super();
- this.setMessage(message);
- this.setColor(color);
+class JokeTest {
+ @Test
+ @Throws(ModuleException::class)
+ fun testRandomJoke() {
+ assertThat(randomJoke().msg, "randomJoke() > 0").all {
+ isNotEmpty()
+ contains("chuck", true)
+ }
}
}
diff --git a/src/test/java/net/thauvin/erik/mobibot/modules/CalcTest.java b/src/test/kotlin/net/thauvin/erik/mobibot/modules/LookupTest.kt
similarity index 63%
rename from src/test/java/net/thauvin/erik/mobibot/modules/CalcTest.java
rename to src/test/kotlin/net/thauvin/erik/mobibot/modules/LookupTest.kt
index 0d5de03..b915357 100644
--- a/src/test/java/net/thauvin/erik/mobibot/modules/CalcTest.java
+++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/LookupTest.kt
@@ -1,7 +1,7 @@
/*
- * CalcTest.java
+ * LookupTest.kt
*
- * Copyright (c) 2004-2019, Erik C. Thauvin (erik@thauvin.net)
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -29,31 +29,33 @@
* 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.modules
-package net.thauvin.erik.mobibot.modules;
-
-import org.testng.annotations.Test;
-
-import static org.assertj.core.api.Assertions.assertThat;
+import assertk.assertThat
+import assertk.assertions.any
+import assertk.assertions.contains
+import net.thauvin.erik.mobibot.modules.Lookup.Companion.nslookup
+import net.thauvin.erik.mobibot.modules.Lookup.Companion.whois
+import org.testng.annotations.Test
/**
- * The CalcTest class.
- *
- * @author Erik C. Thauvin
- * @created 2019-04-07
- * @since 1.0
+ * The `Lookup Test` class.
*/
-public class CalcTest {
+class LookupTest {
@Test
- public void testCalc() {
- assertThat(Calc.calc("1 + 1")).as("calc(1+1)").isEqualTo("1+1 = 2");
- assertThat(Calc.calc("1 -3")).as("calc(1 -3)").isEqualTo("1-3 = -2");
- assertThat(Calc.calc("pi+π+e+φ")).as("calc(pi+π+e+φ)").isEqualTo("pi+π+e+φ = 10.62");
- assertThat(Calc.calc("one + one")).as("calc(one + one)").startsWith("No idea.");
+ @Throws(Exception::class)
+ fun testLookup() {
+ var result = nslookup("apple.com")
+ assertThat(result, "lookup(apple.com)").contains("17.253.144.10")
+
+ result = nslookup("204.122.17.9")
+ assertThat(result, "lookup(204.122.17.9)").contains("nix3.thauvin.us")
}
@Test
- public void testCalcImpl() {
- AbstractModuleTest.testAbstractModule(new Calc());
+ @Throws(Exception::class)
+ fun testWhois() {
+ val result = whois("17.178.96.59", Lookup.WHOIS_HOST)
+ assertThat(result, "whois(17.178.96.59/Apple Inc.").any { it.contains("Apple Inc.") }
}
}
diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/modules/ModuleExceptionTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/modules/ModuleExceptionTest.kt
new file mode 100644
index 0000000..37ba63b
--- /dev/null
+++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/ModuleExceptionTest.kt
@@ -0,0 +1,106 @@
+/*
+ * ModuleExceptionTest.kt
+ *
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package net.thauvin.erik.mobibot.modules
+
+import assertk.all
+import assertk.assertThat
+import assertk.assertions.contains
+import assertk.assertions.doesNotContain
+import assertk.assertions.endsWith
+import assertk.assertions.hasMessage
+import assertk.assertions.isEqualTo
+import assertk.assertions.isNotNull
+import assertk.assertions.isNull
+import net.thauvin.erik.mobibot.ExceptionSanitizer.sanitize
+import org.testng.annotations.DataProvider
+import org.testng.annotations.Test
+import java.io.IOException
+import java.lang.reflect.Method
+
+/**
+ * The `ModuleExceptionTest` class.
+ */
+class ModuleExceptionTest {
+ companion object {
+ const val debugMessage = "debugMessage"
+ const val message = "message"
+ }
+
+ @DataProvider(name = "dp")
+ fun createData(@Suppress("UNUSED_PARAMETER") m: Method?): Array> {
+ return arrayOf(
+ arrayOf(ModuleException(debugMessage, message, IOException("URL http://foobar.com"))),
+ arrayOf(ModuleException(debugMessage, message, IOException("URL http://foobar.com?"))),
+ arrayOf(ModuleException(debugMessage, message))
+ )
+ }
+
+ @Test(dataProvider = "dp")
+ fun testGetDebugMessage(e: ModuleException) {
+ assertThat(e.debugMessage, "get debug message").isEqualTo(debugMessage)
+ }
+
+ @Test(dataProvider = "dp")
+ fun testGetMessage(e: ModuleException) {
+ assertThat(e, "get message").hasMessage(message)
+ }
+
+ @Test
+ fun testSanitizeMessage() {
+ val apiKey = "1234567890"
+ var e = ModuleException(debugMessage, message, IOException("URL http://foo.com?apiKey=$apiKey&userID=me"))
+ assertThat(e.sanitize(apiKey, "", "me").message, "sanitized url").isNotNull().all {
+ contains("xxxxxxxxxx", "userID=xx", "java.io.IOException")
+ doesNotContain(apiKey, "me")
+ }
+
+ e = ModuleException(debugMessage, message, null)
+ assertThat(e.sanitize(apiKey), "no cause").hasMessage(message)
+
+ e = ModuleException(debugMessage, message, IOException())
+ assertThat(e.sanitize(apiKey), "no cause message").hasMessage(message)
+
+ e = ModuleException(debugMessage, apiKey)
+ assertThat(e.sanitize(apiKey).message, "api key in message").isNotNull().doesNotContain(apiKey)
+
+ val msg: String? = null
+ e = ModuleException(debugMessage, msg, IOException(msg))
+ assertThat(e.sanitize(apiKey).message, "null message").isNull()
+
+ e = ModuleException(debugMessage, msg, IOException("foo is $apiKey"))
+ assertThat(e.sanitize(" ", apiKey, "foo").message, "key in cause").isNotNull().all {
+ doesNotContain(apiKey)
+ endsWith("xxx is xxxxxxxxxx")
+ }
+ assertThat(e.sanitize(), "empty").isEqualTo(e)
+ }
+}
diff --git a/src/test/java/net/thauvin/erik/mobibot/modules/PingTest.java b/src/test/kotlin/net/thauvin/erik/mobibot/modules/PingTest.kt
similarity index 72%
rename from src/test/java/net/thauvin/erik/mobibot/modules/PingTest.java
rename to src/test/kotlin/net/thauvin/erik/mobibot/modules/PingTest.kt
index 8dc6c8e..247bf94 100644
--- a/src/test/java/net/thauvin/erik/mobibot/modules/PingTest.java
+++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/PingTest.kt
@@ -1,7 +1,7 @@
/*
- * PingTest.java
+ * PingTest.kt
*
- * Copyright (c) 2004-2019, Erik C. Thauvin (erik@thauvin.net)
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -29,28 +29,27 @@
* 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.modules
-package net.thauvin.erik.mobibot.modules;
-
-import org.testng.annotations.Test;
-
-import static org.assertj.core.api.Assertions.assertThat;
+import assertk.assertThat
+import assertk.assertions.contains
+import assertk.assertions.isNotEmpty
+import net.thauvin.erik.mobibot.modules.Ping.Companion.randomPing
+import org.testng.annotations.Test
/**
- * The PingTest class.
- *
- * @author Erik C. Thauvin
- * @created 2019-04-07
- * @since 1.0
+ * The `PingTest` class.
*/
-public class PingTest {
+class PingTest {
@Test
- public void testPingImpl() {
- AbstractModuleTest.testAbstractModule(new Ping());
+ fun testPingsArray() {
+ assertThat(Ping.PINGS, "Pings array is not empty.").isNotEmpty()
}
@Test
- public void testPingsArray() {
- assertThat(Ping.PINGS).as("Pings array is not empty.").isNotEmpty();
+ fun testRandomPing() {
+ for (i in 0..9) {
+ assertThat(Ping.PINGS, "Random ping $i").contains(randomPing())
+ }
}
}
diff --git a/src/test/java/net/thauvin/erik/mobibot/modules/RockPaperScissorsTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/modules/RockPaperScissorsTest.kt
similarity index 62%
rename from src/test/java/net/thauvin/erik/mobibot/modules/RockPaperScissorsTest.kt
rename to src/test/kotlin/net/thauvin/erik/mobibot/modules/RockPaperScissorsTest.kt
index 6ca4a84..66e95d0 100644
--- a/src/test/java/net/thauvin/erik/mobibot/modules/RockPaperScissorsTest.kt
+++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/RockPaperScissorsTest.kt
@@ -1,7 +1,7 @@
/*
* RockPaperScissorsTest.kt
*
- * Copyright (c) 2004-2020, Erik C. Thauvin (erik@thauvin.net)
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -32,26 +32,19 @@
package net.thauvin.erik.mobibot.modules
-
-import org.assertj.core.api.Assertions.assertThat
+import assertk.assertThat
+import assertk.assertions.isEqualTo
import org.testng.annotations.Test
class RockPaperScissorsTest {
@Test
fun testWinLoseOrDraw() {
- assertThat(
- RockPaperScissors.winLoseOrDraw("scissors", "paper")).`as`("scissors vs. paper").isEqualTo("win")
- assertThat(
- RockPaperScissors.winLoseOrDraw("paper", "rock")).`as`("paper vs. rock").isEqualTo("win")
- assertThat(
- RockPaperScissors.winLoseOrDraw("rock", "scissors")).`as`("rock vs. scissors").isEqualTo("win")
- assertThat(
- RockPaperScissors.winLoseOrDraw("paper", "scissors")).`as`("paper vs. scissors").isEqualTo("lose")
- assertThat(
- RockPaperScissors.winLoseOrDraw("rock", "paper")).`as`("rock vs. paper").isEqualTo("lose")
- assertThat(
- RockPaperScissors.winLoseOrDraw("scissors", "rock")).`as`("scissors vs. rock").isEqualTo("lose")
- assertThat(
- RockPaperScissors.winLoseOrDraw("scissors", "scissors")).`as`("scissors vs. scissors").isEqualTo("draw")
+ assertThat(RockPaperScissors.winLoseOrDraw("scissors", "paper"), "scissors vs. paper").isEqualTo("win")
+ assertThat(RockPaperScissors.winLoseOrDraw("paper", "rock"), "paper vs. rock").isEqualTo("win")
+ assertThat(RockPaperScissors.winLoseOrDraw("rock", "scissors"), "rock vs. scissors").isEqualTo("win")
+ assertThat(RockPaperScissors.winLoseOrDraw("paper", "scissors"), "paper vs. scissors").isEqualTo("lose")
+ assertThat(RockPaperScissors.winLoseOrDraw("rock", "paper"), "rock vs. paper").isEqualTo("lose")
+ assertThat(RockPaperScissors.winLoseOrDraw("scissors", "rock"), "scissors vs. rock").isEqualTo("lose")
+ assertThat(RockPaperScissors.winLoseOrDraw("scissors", "scissors"), "scissors vs. scissors").isEqualTo("draw")
}
}
diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/modules/StockQuoteTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/modules/StockQuoteTest.kt
new file mode 100644
index 0000000..57dcb4c
--- /dev/null
+++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/StockQuoteTest.kt
@@ -0,0 +1,88 @@
+/*
+ * StockQuoteTest.kt
+ *
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package net.thauvin.erik.mobibot.modules
+
+import assertk.all
+import assertk.assertThat
+import assertk.assertions.hasNoCause
+import assertk.assertions.isEqualTo
+import assertk.assertions.isFailure
+import assertk.assertions.isInstanceOf
+import assertk.assertions.isNotEmpty
+import assertk.assertions.matches
+import assertk.assertions.prop
+import net.thauvin.erik.mobibot.ExceptionSanitizer.sanitize
+import net.thauvin.erik.mobibot.LocalProperties
+import net.thauvin.erik.mobibot.modules.StockQuote.Companion.getQuote
+import net.thauvin.erik.mobibot.msg.ErrorMessage
+import net.thauvin.erik.mobibot.msg.Message
+import org.testng.annotations.Test
+
+/**
+ * The `StockQuoteTest` class.
+ */
+class StockQuoteTest : LocalProperties() {
+ private fun buildMatch(label: String): String {
+ return "${label}:[ ]+[0-9.]+".prependIndent()
+ }
+
+ @Test
+ @Throws(ModuleException::class)
+ fun testGetQuote() {
+ val apiKey = getProperty(StockQuote.ALPHAVANTAGE_API_KEY_PROP)
+ try {
+ val messages = getQuote("apple inc", apiKey)
+ assertThat(messages, "response not empty").isNotEmpty()
+ assertThat(messages[0].msg, "same stock symbol").matches("Symbol: AAPL .*".toRegex())
+ assertThat(messages[1].msg, "price label").matches(buildMatch("Price").toRegex())
+ assertThat(messages[2].msg, "previous label").matches(buildMatch("Previous").toRegex())
+ assertThat(messages[3].msg, "open label").matches(buildMatch("Open").toRegex())
+
+ assertThat(getQuote("blahfoo", apiKey).first(), "invalid symbol").all {
+ isInstanceOf(ErrorMessage::class.java)
+ prop(Message::msg).isEqualTo(StockQuote.INVALID_SYMBOL)
+ }
+ assertThat(getQuote("", "apikey").first(), "empty symbol").all {
+ isInstanceOf(ErrorMessage::class.java)
+ prop(Message::msg).isEqualTo(StockQuote.INVALID_SYMBOL)
+ }
+ assertThat { getQuote("test", "") }.isFailure().isInstanceOf(ModuleException::class.java).hasNoCause()
+ } catch (e: ModuleException) {
+ // Avoid displaying api keys in CI logs
+ if ("true" == System.getenv("CI")) {
+ throw e.sanitize(apiKey)
+ } else {
+ throw e
+ }
+ }
+ }
+}
diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/modules/TwitterTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/modules/TwitterTest.kt
new file mode 100644
index 0000000..29ae3c4
--- /dev/null
+++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/TwitterTest.kt
@@ -0,0 +1,73 @@
+/*
+ * TwitterTest.kt
+ *
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package net.thauvin.erik.mobibot.modules
+
+import assertk.assertThat
+import assertk.assertions.isEqualTo
+import assertk.assertions.isSuccess
+import net.thauvin.erik.mobibot.LocalProperties
+import net.thauvin.erik.mobibot.modules.Twitter.Companion.twitterPost
+import org.testng.annotations.Test
+import java.net.InetAddress
+import java.net.UnknownHostException
+
+/**
+ * The `TwitterTest` class.
+ */
+class TwitterTest : LocalProperties() {
+ private val ci: String
+ get() {
+ val ciName = System.getenv("CI_NAME")
+ return ciName ?: try {
+ InetAddress.getLocalHost().hostName
+ } catch (ignore: UnknownHostException) {
+ "Unknown Host"
+ }
+ }
+
+ @Test
+ @Throws(ModuleException::class)
+ fun testPostTwitter() {
+ val msg = "Testing Twitter API from $ci"
+ assertThat {
+ twitterPost(
+ getProperty(Twitter.CONSUMER_KEY_PROP),
+ getProperty(Twitter.CONSUMER_SECRET_PROP),
+ getProperty(Twitter.TOKEN_PROP),
+ getProperty(Twitter.TOKEN_SECRET_PROP),
+ getProperty(Twitter.HANDLE_PROP),
+ msg,
+ true
+ )
+ }.isSuccess().isEqualTo(msg)
+ }
+}
diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/modules/Weather2Test.kt b/src/test/kotlin/net/thauvin/erik/mobibot/modules/Weather2Test.kt
new file mode 100644
index 0000000..315fa06
--- /dev/null
+++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/Weather2Test.kt
@@ -0,0 +1,120 @@
+/*
+ * Weather2Test.kt
+ *
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package net.thauvin.erik.mobibot.modules
+
+import assertk.all
+import assertk.assertThat
+import assertk.assertions.contains
+import assertk.assertions.endsWith
+import assertk.assertions.hasNoCause
+import assertk.assertions.isEqualTo
+import assertk.assertions.isFailure
+import assertk.assertions.isInstanceOf
+import assertk.assertions.isNotNull
+import assertk.assertions.isTrue
+import net.aksingh.owmjapis.api.APIException
+import net.aksingh.owmjapis.core.OWM
+import net.thauvin.erik.mobibot.LocalProperties
+import net.thauvin.erik.mobibot.modules.Weather2.Companion.OWM_API_KEY_PROP
+import net.thauvin.erik.mobibot.modules.Weather2.Companion.ftoC
+import net.thauvin.erik.mobibot.modules.Weather2.Companion.getCountry
+import net.thauvin.erik.mobibot.modules.Weather2.Companion.getWeather
+import net.thauvin.erik.mobibot.modules.Weather2.Companion.mphToKmh
+import org.testng.annotations.Test
+
+/**
+ * The `Weather2Test` class.
+ */
+class Weather2Test : LocalProperties() {
+ @Test
+ fun testFtoC() {
+ val t = ftoC(32.0)
+ assertThat(t.second, "32 °F is 0 °C").isEqualTo(0)
+ }
+
+ @Test
+ fun testGetCountry() {
+ assertThat(getCountry("foo"), "not a country").isEqualTo(OWM.Country.UNITED_STATES)
+ assertThat(getCountry("fr"), "fr is france").isEqualTo(OWM.Country.FRANCE)
+
+ val country = OWM.Country.values()
+ repeat(3) {
+ val rand = country[(country.indices).random()]
+ assertThat(getCountry(rand.value), rand.name).isEqualTo(rand)
+ }
+ }
+
+ @Test
+ fun testMphToKmh() {
+ val w = mphToKmh(0.62)
+ assertThat(w.second, "0.62 mph is 1 km/h").isEqualTo(1)
+ }
+
+ @Test
+ @Throws(ModuleException::class)
+ fun testWeather() {
+ var messages = getWeather("98204", getProperty(OWM_API_KEY_PROP))
+ assertThat(messages[0].msg, "is Everett").all {
+ contains("Everett, United States")
+ contains("US")
+ }
+ assertThat(messages[messages.size - 1].msg, "is Everett zip code").endsWith("98204%2CUS")
+
+ messages = getWeather("San Francisco", getProperty(OWM_API_KEY_PROP))
+ assertThat(messages[0].msg, "is San Francisco").all {
+ contains("San Francisco")
+ contains("US")
+ }
+ assertThat(messages[messages.size - 1].msg, "is San Fran city code").endsWith("5391959")
+
+ messages = getWeather("London, GB", getProperty(OWM_API_KEY_PROP))
+ assertThat(messages[0].msg, "is UK").all {
+ contains("London, United Kingdom")
+ contains("GB")
+ }
+ assertThat(messages[messages.size - 1].msg, "is London city code").endsWith("2643743")
+
+ try {
+ getWeather("Foo, US", getProperty(OWM_API_KEY_PROP))
+ } catch (e: ModuleException) {
+ assertThat(e.cause, "cause is API exception").isNotNull().isInstanceOf(APIException::class.java)
+ }
+
+ assertThat { getWeather("test", "") }.isFailure()
+ .isInstanceOf(ModuleException::class.java).hasNoCause()
+ assertThat { getWeather("test", null) }.isFailure()
+ .isInstanceOf(ModuleException::class.java).hasNoCause()
+
+ messages = getWeather("", "apikey")
+ assertThat(messages[0].isError, "no query").isTrue()
+ }
+}
diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/modules/WordTimeTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/modules/WordTimeTest.kt
new file mode 100644
index 0000000..09ac600
--- /dev/null
+++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/WordTimeTest.kt
@@ -0,0 +1,72 @@
+/*
+ * WordTimeTest.kt
+ *
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package net.thauvin.erik.mobibot.modules
+
+import assertk.assertThat
+import assertk.assertions.endsWith
+import assertk.assertions.isSuccess
+import assertk.assertions.matches
+import assertk.assertions.startsWith
+import net.thauvin.erik.mobibot.Utils.bold
+import net.thauvin.erik.mobibot.modules.WorldTime.Companion.BEATS_KEYWORD
+import net.thauvin.erik.mobibot.modules.WorldTime.Companion.COUNTRIES_MAP
+import net.thauvin.erik.mobibot.modules.WorldTime.Companion.time
+import org.pircbotx.Colors
+import org.testng.annotations.Test
+import java.time.ZoneId
+
+/**
+ * The `WordTimeTest` class.
+ */
+class WordTimeTest {
+ @Test
+ fun testTime() {
+ assertThat(time(), "no zone").matches(
+ ("The time is ${Colors.BOLD}\\d{1,2}:\\d{2}${Colors.BOLD} " +
+ "on ${Colors.BOLD}\\w+, \\d{1,2} \\w+ \\d{4}${Colors.BOLD} " +
+ "in ${Colors.BOLD}Los Angeles${Colors.BOLD}").toRegex()
+ )
+ assertThat(time(""), "empty zone").endsWith("Los Angeles".bold())
+ assertThat(time("PST"), "PST").endsWith("Los Angeles".bold())
+ assertThat(time("GB"), "GB").endsWith("London".bold())
+ assertThat(time("FR"), "FR").endsWith("Paris".bold())
+ assertThat(time("BLAH"), "BLAH").startsWith("Unsupported")
+ assertThat(time("BEAT"), BEATS_KEYWORD).matches("[\\w ]+ .?@\\d{3}+.? .beats".toRegex())
+ }
+
+ @Test
+ fun testZones() {
+ COUNTRIES_MAP.filter { it.value != BEATS_KEYWORD }.forEach {
+ assertThat { ZoneId.of(it.value) }.isSuccess()
+ }
+ }
+}
diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/msg/TestMessage.kt b/src/test/kotlin/net/thauvin/erik/mobibot/msg/TestMessage.kt
new file mode 100644
index 0000000..ef4be2c
--- /dev/null
+++ b/src/test/kotlin/net/thauvin/erik/mobibot/msg/TestMessage.kt
@@ -0,0 +1,93 @@
+/*
+ * TestMessage.kt
+ *
+ * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package net.thauvin.erik.mobibot.msg
+
+import assertk.all
+import assertk.assertThat
+import assertk.assertions.isFalse
+import assertk.assertions.isTrue
+import assertk.assertions.prop
+import org.testng.annotations.Test
+
+class TestMessage {
+ @Test
+ fun testConstructor() {
+ var msg = Message("foo")
+
+ msg.isError = true
+ assertThat(msg.isNotice, "message is notice").isTrue()
+
+ msg = Message("foo", isError = true)
+ assertThat(msg.isNotice, "message is notice too").isTrue()
+ }
+
+ @Test
+ fun testErrorMessage() {
+ val msg = ErrorMessage("foo")
+ assertThat(msg).all {
+ prop(Message::isError).isTrue()
+ prop(Message::isNotice).isTrue()
+ prop(Message::isPrivate).isFalse()
+ }
+ }
+
+ @Test
+ fun testNoticeMessage() {
+ val msg = NoticeMessage("food")
+ assertThat(msg).all {
+ prop(Message::isError).isFalse()
+ prop(Message::isNotice).isTrue()
+ prop(Message::isPrivate).isFalse()
+ }
+ }
+
+ @Test
+ fun testPrivateMessage() {
+ val msg = PrivateMessage("foo")
+ assertThat(msg).all {
+ prop(Message::isPrivate).isTrue()
+ prop(Message::isError).isFalse()
+ prop(Message::isNotice).isFalse()
+ }
+ }
+
+ @Test
+ fun testPublicMessage() {
+ val msg = PublicMessage("foo")
+ assertThat(msg).all {
+ prop(Message::isError).isFalse()
+ prop(Message::isNotice).isFalse()
+ prop(Message::isPrivate).isFalse()
+ }
+ }
+}
diff --git a/src/test/resources/current.xml b/src/test/resources/current.xml
new file mode 100644
index 0000000..8552a9a
--- /dev/null
+++ b/src/test/resources/current.xml
@@ -0,0 +1,36 @@
+
+
+
+ #mobibot IRC Links
+ https://www.mobitopia.org/mobibot/logs
+ Links from irc.example.com on #mobibot
+ en
+ Sun, 31 Oct 2021 21:45:11 GMT
+ 2021-10-31T21:45:11Z
+ en
+
+ Example 2
+ https://www.example.com/2
+ Posted by <b>Skynx</b> on <a href="irc://irc.libera.chat/#mobibot"><b>#mobibot</b></a>
+ tag2-1
+ tag2-2
+ Sun, 31 Oct 2021 21:45:11 GMT
+ https://www.foo.com
+ mobibot@irc.libera.chat (Skynx)
+ 2021-10-31T00:01:00Z
+
+
+ Example 1
+ https://www.example.com/1
+ Posted by <b>ErikT</b> on <a href="irc://irc.libera.chat/#mobibot"><b>#mobibot</b></a>
+ <br/><br/>ErikT: This is comment 1. <br/>Skynx: This is comment 2.
+
+ tag1-1
+ tag1-2
+ Sun, 31 Oct 2021 21:43:15 GMT
+ https://www.example.com/
+ mobibot@irc.libera.chat (ErikT)
+ 2021-10-31T00:00:00Z
+
+
+
diff --git a/version.mustache b/version.mustache
index 5314f35..b2672e4 100644
--- a/version.mustache
+++ b/version.mustache
@@ -1,31 +1,32 @@
/*
- * This file is automatically generated.
- * Do not modify! -- ALL CHANGES WILL BE ERASED!
- */
-package {{packageName}};
+* This file is automatically generated.
+* Do not modify! -- ALL CHANGES WILL BE ERASED!
+*/
+package {{packageName}}
-import java.time.*;
+import java.time.LocalDateTime
+import java.time.ZoneId
+import java.time.Instant
/**
- * Provides semantic version information.
- *
- * @author Semantic Version Annotation Processor
- */
-public final class {{className}} {
- public static final String PROJECT = "{{project}}";
- public static final LocalDateTime BUILDDATE =
- LocalDateTime.ofInstant(Instant.ofEpochMilli({{epoch}}L), ZoneId.systemDefault());
- public static final int MAJOR = {{major}};
- public static final int MINOR = {{minor}};
- public static final int PATCH = {{patch}};
- public static final String PRERELEASE = "{{preRelease}}";
- public static final String BUILDMETA = "{{buildMeta}}";
- public static final String VERSION = "{{version}}";
+* Provides semantic version information.
+*
+* @author [Semantic Version Annotation Processor](https://github.com/ethauvin/semver)
+*/
+object ReleaseInfo{
+ const val PROJECT = "{{project}}"
+ const val VERSION = "{{version}}"
+
+ @JvmField
+ val BUILDDATE = LocalDateTime.ofInstant(Instant.ofEpochMilli({{epoch}}L), ZoneId.systemDefault())
- /**
- * Disables the default constructor.
- */
- private {{className}}() {
- throw new UnsupportedOperationException("Illegal constructor call.");
- }
+ const val MAJOR = {{major}}
+ const val MINOR = {{minor}}
+ const val PATCH = {{patch}}
+ const val BUILDMETA = "{{buildMeta}}"
+ const val PRERELEASE = "{{preRelease}}"
+
+ const val WEBSITE = "https://www.mobitopia.org/mobibot/"
+ const val AUTHOR = "Erik C. Thauvin"
+ const val AUTHOR_URL = "https://erik.thauvin.net/"
}
diff --git a/version.properties b/version.properties
index aa42206..2f05c8d 100644
--- a/version.properties
+++ b/version.properties
@@ -1,9 +1,9 @@
#Generated by the Semver Plugin for Gradle
-#Mon Mar 23 23:04:40 PDT 2020
-version.buildmeta=714
+#Wed Jan 19 15:20:26 PST 2022
+version.buildmeta=001
version.major=0
-version.minor=7
-version.patch=3
-version.prerelease=beta
+version.minor=8
+version.patch=0
+version.prerelease=rc
version.project=mobibot
-version.semver=0.7.3-beta+714
+version.semver=0.8.0-rc+001
diff --git a/website/index.html b/website/index.html
index a934ecd..6427843 100644
--- a/website/index.html
+++ b/website/index.html
@@ -33,20 +33,19 @@