From c68c25aa9504a4217cb9495b8086ea9d795a5aa6 Mon Sep 17 00:00:00 2001 From: "Erik C. Thauvin" Date: Fri, 10 Nov 2023 23:43:20 -0800 Subject: [PATCH] Moved from Gradle to bld --- .circleci/config.yml | 42 +- .github/workflows/gradle.yml | 48 +- .gitignore | 150 +++--- .gitlab-ci.yml | 31 +- .idea/app.iml | 30 ++ .idea/bld.iml | 14 + .idea/codeStyles/Project.xml | 298 ------------ .idea/codeStyles/codeStyleConfig.xml | 6 - .idea/compiler.xml | 6 - ...{Erik_s_Copyright_Notice.xml => BSD_3.xml} | 4 +- .idea/copyright/profiles_settings.xml | 2 +- .idea/inspectionProfiles/Project_Default.xml | 64 --- .idea/jarRepositories.xml | 45 -- .idea/kotlinc.xml | 6 - .idea/libraries/bld.xml | 17 + .idea/libraries/compile.xml | 13 + .idea/libraries/runtime.xml | 14 + .idea/libraries/test.xml | 14 + .idea/misc.xml | 18 +- .idea/modules.xml | 9 + .idea/runConfigurations/Run Tests.xml | 9 + .vscode/launch.json | 11 + .vscode/settings.json | 15 + README.md | 6 +- bin/main/log4j2.xml | 69 +++ bin/main/net/thauvin/erik/mobibot/Addons.kt | 190 ++++++++ .../net/thauvin/erik/mobibot/Constants.kt | 102 ++++ .../net/thauvin/erik/mobibot/FeedReader.kt | 92 ++++ bin/main/net/thauvin/erik/mobibot/Mobibot.kt | 421 +++++++++++++++++ bin/main/net/thauvin/erik/mobibot/Pinboard.kt | 113 +++++ bin/main/net/thauvin/erik/mobibot/Utils.kt | 439 ++++++++++++++++++ .../erik/mobibot/commands/AbstractCommand.kt | 79 ++++ .../erik/mobibot/commands/ChannelFeed.kt | 62 +++ .../thauvin/erik/mobibot/commands/Cycle.kt | 66 +++ .../net/thauvin/erik/mobibot/commands/Die.kt | 62 +++ .../thauvin/erik/mobibot/commands/Ignore.kt | 147 ++++++ .../net/thauvin/erik/mobibot/commands/Info.kt | 124 +++++ .../net/thauvin/erik/mobibot/commands/Me.kt | 51 ++ .../thauvin/erik/mobibot/commands/Modules.kt | 63 +++ .../net/thauvin/erik/mobibot/commands/Msg.kt | 60 +++ .../net/thauvin/erik/mobibot/commands/Nick.kt | 51 ++ .../thauvin/erik/mobibot/commands/Recap.kt | 81 ++++ .../net/thauvin/erik/mobibot/commands/Say.kt | 51 ++ .../thauvin/erik/mobibot/commands/Users.kt | 50 ++ .../thauvin/erik/mobibot/commands/Versions.kt | 59 +++ .../erik/mobibot/commands/links/Comment.kt | 151 ++++++ .../mobibot/commands/links/LinksManager.kt | 207 +++++++++ .../erik/mobibot/commands/links/Posting.kt | 164 +++++++ .../erik/mobibot/commands/links/Tags.kt | 87 ++++ .../erik/mobibot/commands/links/View.kt | 120 +++++ .../mobibot/commands/seen/NickComparator.kt | 45 ++ .../erik/mobibot/commands/seen/Seen.kt | 150 ++++++ .../erik/mobibot/commands/seen/SeenNick.kt | 41 ++ .../erik/mobibot/commands/tell/Tell.kt | 306 ++++++++++++ .../erik/mobibot/commands/tell/TellManager.kt | 74 +++ .../erik/mobibot/commands/tell/TellMessage.kt | 104 +++++ .../thauvin/erik/mobibot/entries/Entries.kt | 54 +++ .../erik/mobibot/entries/EntriesUtils.kt | 83 ++++ .../erik/mobibot/entries/EntryComment.kt | 52 +++ .../thauvin/erik/mobibot/entries/EntryLink.kt | 213 +++++++++ .../erik/mobibot/entries/FeedsManager.kt | 187 ++++++++ .../erik/mobibot/modules/AbstractModule.kt | 131 ++++++ .../net/thauvin/erik/mobibot/modules/Calc.kt | 87 ++++ .../thauvin/erik/mobibot/modules/ChatGpt.kt | 176 +++++++ .../erik/mobibot/modules/CryptoPrices.kt | 159 +++++++ .../erik/mobibot/modules/CurrencyConverter.kt | 222 +++++++++ .../net/thauvin/erik/mobibot/modules/Dice.kt | 87 ++++ .../erik/mobibot/modules/GoogleSearch.kt | 162 +++++++ .../net/thauvin/erik/mobibot/modules/Joke.kt | 105 +++++ .../thauvin/erik/mobibot/modules/Lookup.kt | 171 +++++++ .../thauvin/erik/mobibot/modules/Mastodon.kt | 149 ++++++ .../erik/mobibot/modules/ModuleException.kt | 45 ++ .../net/thauvin/erik/mobibot/modules/Ping.kt | 83 ++++ .../erik/mobibot/modules/RockPaperScissors.kt | 114 +++++ .../erik/mobibot/modules/StockQuote.kt | 236 ++++++++++ .../thauvin/erik/mobibot/modules/War.class | Bin 0 -> 2452 bytes .../thauvin/erik/mobibot/modules/Weather2.kt | 250 ++++++++++ .../erik/mobibot/modules/WolframAlpha.kt | 142 ++++++ .../thauvin/erik/mobibot/modules/WorldTime.kt | 390 ++++++++++++++++ .../thauvin/erik/mobibot/msg/ErrorMessage.kt | 37 ++ .../net/thauvin/erik/mobibot/msg/Message.kt | 65 +++ .../thauvin/erik/mobibot/msg/NoticeMessage.kt | 38 ++ .../erik/mobibot/msg/PrivateMessage.kt | 37 ++ .../thauvin/erik/mobibot/msg/PublicMessage.kt | 36 ++ .../erik/mobibot/social/SocialManager.kt | 116 +++++ .../erik/mobibot/social/SocialModule.kt | 96 ++++ .../erik/mobibot/social/SocialTimer.kt | 40 ++ bin/test/current.xml | 67 +++ .../net/thauvin/erik/mobibot/AddonsTest.kt | 86 ++++ .../erik/mobibot/ExceptionSanitizer.kt | 60 +++ .../thauvin/erik/mobibot/FeedReaderTest.kt | 78 ++++ .../thauvin/erik/mobibot/LocalProperties.kt | 85 ++++ .../net/thauvin/erik/mobibot/PinboardTest.kt | 81 ++++ .../net/thauvin/erik/mobibot/UtilsTest.kt | 278 +++++++++++ .../thauvin/erik/mobibot/commands/InfoTest.kt | 58 +++ .../erik/mobibot/commands/RecapTest.kt | 60 +++ .../commands/links/LinksManagerTest.kt | 77 +++ .../erik/mobibot/commands/links/ViewTest.kt | 111 +++++ .../erik/mobibot/commands/seen/SeenTest.kt | 85 ++++ .../mobibot/commands/tell/TellMessageTest.kt | 72 +++ .../commands/tell/TellMessagesMgrTest.kt | 87 ++++ .../erik/mobibot/entries/EntriesUtilsTest.kt | 91 ++++ .../erik/mobibot/entries/EntryLinkTest.kt | 133 ++++++ .../erik/mobibot/entries/FeedMgrTest.kt | 115 +++++ .../thauvin/erik/mobibot/modules/CalcTest.kt | 53 +++ .../erik/mobibot/modules/ChatGptTest.kt | 62 +++ .../erik/mobibot/modules/CryptoPricesTest.kt | 78 ++++ .../mobibot/modules/CurrencyConverterTest.kt | 85 ++++ .../thauvin/erik/mobibot/modules/DiceTest.kt | 53 +++ .../erik/mobibot/modules/GoogleSearchTest.kt | 95 ++++ .../thauvin/erik/mobibot/modules/JokeTest.kt | 57 +++ .../erik/mobibot/modules/LookupTest.kt | 60 +++ .../erik/mobibot/modules/MastodonTest.kt | 54 +++ .../mobibot/modules/ModuleExceptionTest.kt | 105 +++++ .../thauvin/erik/mobibot/modules/PingTest.kt | 54 +++ .../mobibot/modules/RockPaperScissorsTest.kt | 50 ++ .../erik/mobibot/modules/StockQuoteTest.kt | 85 ++++ .../erik/mobibot/modules/Weather2Test.kt | 117 +++++ .../erik/mobibot/modules/WolframAlphaTest.kt | 77 +++ .../erik/mobibot/modules/WordTimeTest.kt | 70 +++ .../thauvin/erik/mobibot/msg/MessageTest.kt | 109 +++++ bitbucket-pipelines.yml | 9 +- bld | 2 + bld.bat | 4 + build.gradle | 247 ---------- deploy.sh | 2 +- gradle.properties | 0 gradle/wrapper/gradle-wrapper.jar | Bin 63721 -> 0 bytes gradle/wrapper/gradle-wrapper.properties | 7 - gradlew | 249 ---------- gradlew.bat | 92 ---- lib/bld/bld-wrapper.jar | Bin 0 -> 27321 bytes lib/bld/bld-wrapper.properties | 10 + release_info.txt | 27 ++ settings.gradle | 18 - sonar-project.properties | 7 + .../java/net/thauvin/erik/MobibotBuild.java | 170 +++++++ .../net/thauvin/erik/mobibot/modules/War.java | 108 ----- .../kotlin/net/thauvin/erik/mobibot/Addons.kt | 2 +- .../net/thauvin/erik/mobibot/Constants.kt | 2 +- .../net/thauvin/erik/mobibot/FeedReader.kt | 2 +- .../net/thauvin/erik/mobibot/Mobibot.kt | 6 +- .../net/thauvin/erik/mobibot/Pinboard.kt | 2 +- .../net/thauvin/erik/mobibot/ReleaseInfo.kt | 27 ++ .../kotlin/net/thauvin/erik/mobibot/Utils.kt | 2 +- .../erik/mobibot/commands/AbstractCommand.kt | 2 +- .../erik/mobibot/commands/ChannelFeed.kt | 2 +- .../thauvin/erik/mobibot/commands/Cycle.kt | 2 +- .../net/thauvin/erik/mobibot/commands/Die.kt | 2 +- .../thauvin/erik/mobibot/commands/Ignore.kt | 2 +- .../net/thauvin/erik/mobibot/commands/Info.kt | 2 +- .../net/thauvin/erik/mobibot/commands/Me.kt | 2 +- .../thauvin/erik/mobibot/commands/Modules.kt | 2 +- .../net/thauvin/erik/mobibot/commands/Msg.kt | 2 +- .../net/thauvin/erik/mobibot/commands/Nick.kt | 2 +- .../thauvin/erik/mobibot/commands/Recap.kt | 2 +- .../net/thauvin/erik/mobibot/commands/Say.kt | 2 +- .../thauvin/erik/mobibot/commands/Users.kt | 2 +- .../thauvin/erik/mobibot/commands/Versions.kt | 4 +- .../erik/mobibot/commands/links/Comment.kt | 2 +- .../mobibot/commands/links/LinksManager.kt | 2 +- .../erik/mobibot/commands/links/Posting.kt | 2 +- .../erik/mobibot/commands/links/Tags.kt | 2 +- .../erik/mobibot/commands/links/View.kt | 2 +- .../mobibot/commands/seen/NickComparator.kt | 2 +- .../erik/mobibot/commands/seen/Seen.kt | 2 +- .../erik/mobibot/commands/seen/SeenNick.kt | 2 +- .../erik/mobibot/commands/tell/Tell.kt | 2 +- .../erik/mobibot/commands/tell/TellManager.kt | 2 +- .../erik/mobibot/commands/tell/TellMessage.kt | 2 +- .../thauvin/erik/mobibot/entries/Entries.kt | 2 +- .../erik/mobibot/entries/EntriesUtils.kt | 2 +- .../erik/mobibot/entries/EntryComment.kt | 2 +- .../thauvin/erik/mobibot/entries/EntryLink.kt | 2 +- .../erik/mobibot/entries/FeedsManager.kt | 2 +- .../erik/mobibot/modules/AbstractModule.kt | 2 +- .../net/thauvin/erik/mobibot/modules/Calc.kt | 2 +- .../thauvin/erik/mobibot/modules/ChatGpt.kt | 2 +- .../erik/mobibot/modules/CryptoPrices.kt | 4 +- .../erik/mobibot/modules/CurrencyConverter.kt | 2 +- .../net/thauvin/erik/mobibot/modules/Dice.kt | 2 +- .../erik/mobibot/modules/GoogleSearch.kt | 2 +- .../net/thauvin/erik/mobibot/modules/Joke.kt | 2 +- .../thauvin/erik/mobibot/modules/Lookup.kt | 2 +- .../thauvin/erik/mobibot/modules/Mastodon.kt | 2 +- .../erik/mobibot/modules/ModuleException.kt | 2 +- .../net/thauvin/erik/mobibot/modules/Ping.kt | 2 +- .../erik/mobibot/modules/RockPaperScissors.kt | 2 +- .../erik/mobibot/modules/StockQuote.kt | 2 +- .../net/thauvin/erik/mobibot/modules/War.kt | 89 ++++ .../thauvin/erik/mobibot/modules/Weather2.kt | 2 +- .../erik/mobibot/modules/WolframAlpha.kt | 2 +- .../thauvin/erik/mobibot/modules/WorldTime.kt | 2 +- .../thauvin/erik/mobibot/msg/ErrorMessage.kt | 2 +- .../net/thauvin/erik/mobibot/msg/Message.kt | 6 +- .../thauvin/erik/mobibot/msg/NoticeMessage.kt | 2 +- .../erik/mobibot/msg/PrivateMessage.kt | 2 +- .../thauvin/erik/mobibot/msg/PublicMessage.kt | 2 +- .../erik/mobibot/social/SocialManager.kt | 2 +- .../erik/mobibot/social/SocialModule.kt | 2 +- .../erik/mobibot/social/SocialTimer.kt | 2 +- src/main/resources/log4j2.xml | 38 ++ .../net/thauvin/erik/mobibot/AddonsTest.kt | 4 +- .../net/thauvin/erik/mobibot/DisableOnCi.kt | 44 ++ .../erik/mobibot/DisableOnCiCondition.kt | 51 ++ .../erik/mobibot/ExceptionSanitizer.kt | 2 +- .../thauvin/erik/mobibot/FeedReaderTest.kt | 4 +- .../thauvin/erik/mobibot/LocalProperties.kt | 6 +- .../net/thauvin/erik/mobibot/PinboardTest.kt | 8 +- .../net/thauvin/erik/mobibot/UtilsTest.kt | 8 +- .../thauvin/erik/mobibot/commands/InfoTest.kt | 6 +- .../erik/mobibot/commands/RecapTest.kt | 6 +- .../commands/links/LinksManagerTest.kt | 10 +- .../erik/mobibot/commands/links/ViewTest.kt | 6 +- .../erik/mobibot/commands/seen/SeenTest.kt | 52 ++- .../mobibot/commands/tell/TellMessageTest.kt | 6 +- .../commands/tell/TellMessagesMgrTest.kt | 30 +- .../erik/mobibot/entries/EntriesUtilsTest.kt | 12 +- .../erik/mobibot/entries/EntryLinkTest.kt | 12 +- .../erik/mobibot/entries/FeedMgrTest.kt | 10 +- .../thauvin/erik/mobibot/modules/CalcTest.kt | 6 +- .../erik/mobibot/modules/ChatGptTest.kt | 10 +- .../erik/mobibot/modules/CryptoPricesTest.kt | 13 +- .../mobibot/modules/CurrencyConverterTest.kt | 12 +- .../thauvin/erik/mobibot/modules/DiceTest.kt | 6 +- .../erik/mobibot/modules/GoogleSearchTest.kt | 10 +- .../thauvin/erik/mobibot/modules/JokeTest.kt | 6 +- .../erik/mobibot/modules/LookupTest.kt | 8 +- .../erik/mobibot/modules/MastodonTest.kt | 6 +- .../mobibot/modules/ModuleExceptionTest.kt | 34 +- .../thauvin/erik/mobibot/modules/PingTest.kt | 8 +- .../mobibot/modules/RockPaperScissorsTest.kt | 6 +- .../erik/mobibot/modules/StockQuoteTest.kt | 6 +- .../erik/mobibot/modules/Weather2Test.kt | 12 +- .../erik/mobibot/modules/WolframAlphaTest.kt | 10 +- .../erik/mobibot/modules/WordTimeTest.kt | 8 +- .../thauvin/erik/mobibot/msg/MessageTest.kt | 4 +- src/test/resources/current.xml | 31 -- version.mustache | 32 -- version.properties | 9 - 240 files changed, 11508 insertions(+), 1650 deletions(-) create mode 100644 .idea/app.iml create mode 100644 .idea/bld.iml delete mode 100644 .idea/codeStyles/Project.xml delete mode 100644 .idea/codeStyles/codeStyleConfig.xml delete mode 100644 .idea/compiler.xml rename .idea/copyright/{Erik_s_Copyright_Notice.xml => BSD_3.xml} (93%) delete mode 100644 .idea/jarRepositories.xml delete mode 100644 .idea/kotlinc.xml create mode 100644 .idea/libraries/bld.xml create mode 100644 .idea/libraries/compile.xml create mode 100644 .idea/libraries/runtime.xml create mode 100644 .idea/libraries/test.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/runConfigurations/Run Tests.xml create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 bin/main/log4j2.xml create mode 100644 bin/main/net/thauvin/erik/mobibot/Addons.kt create mode 100644 bin/main/net/thauvin/erik/mobibot/Constants.kt create mode 100644 bin/main/net/thauvin/erik/mobibot/FeedReader.kt create mode 100644 bin/main/net/thauvin/erik/mobibot/Mobibot.kt create mode 100644 bin/main/net/thauvin/erik/mobibot/Pinboard.kt create mode 100644 bin/main/net/thauvin/erik/mobibot/Utils.kt create mode 100644 bin/main/net/thauvin/erik/mobibot/commands/AbstractCommand.kt create mode 100644 bin/main/net/thauvin/erik/mobibot/commands/ChannelFeed.kt create mode 100644 bin/main/net/thauvin/erik/mobibot/commands/Cycle.kt create mode 100644 bin/main/net/thauvin/erik/mobibot/commands/Die.kt create mode 100644 bin/main/net/thauvin/erik/mobibot/commands/Ignore.kt create mode 100644 bin/main/net/thauvin/erik/mobibot/commands/Info.kt create mode 100644 bin/main/net/thauvin/erik/mobibot/commands/Me.kt create mode 100644 bin/main/net/thauvin/erik/mobibot/commands/Modules.kt create mode 100644 bin/main/net/thauvin/erik/mobibot/commands/Msg.kt create mode 100644 bin/main/net/thauvin/erik/mobibot/commands/Nick.kt create mode 100644 bin/main/net/thauvin/erik/mobibot/commands/Recap.kt create mode 100644 bin/main/net/thauvin/erik/mobibot/commands/Say.kt create mode 100644 bin/main/net/thauvin/erik/mobibot/commands/Users.kt create mode 100644 bin/main/net/thauvin/erik/mobibot/commands/Versions.kt create mode 100644 bin/main/net/thauvin/erik/mobibot/commands/links/Comment.kt create mode 100644 bin/main/net/thauvin/erik/mobibot/commands/links/LinksManager.kt create mode 100644 bin/main/net/thauvin/erik/mobibot/commands/links/Posting.kt create mode 100644 bin/main/net/thauvin/erik/mobibot/commands/links/Tags.kt create mode 100644 bin/main/net/thauvin/erik/mobibot/commands/links/View.kt create mode 100644 bin/main/net/thauvin/erik/mobibot/commands/seen/NickComparator.kt create mode 100644 bin/main/net/thauvin/erik/mobibot/commands/seen/Seen.kt create mode 100644 bin/main/net/thauvin/erik/mobibot/commands/seen/SeenNick.kt create mode 100644 bin/main/net/thauvin/erik/mobibot/commands/tell/Tell.kt create mode 100644 bin/main/net/thauvin/erik/mobibot/commands/tell/TellManager.kt create mode 100644 bin/main/net/thauvin/erik/mobibot/commands/tell/TellMessage.kt create mode 100644 bin/main/net/thauvin/erik/mobibot/entries/Entries.kt create mode 100644 bin/main/net/thauvin/erik/mobibot/entries/EntriesUtils.kt create mode 100644 bin/main/net/thauvin/erik/mobibot/entries/EntryComment.kt create mode 100644 bin/main/net/thauvin/erik/mobibot/entries/EntryLink.kt create mode 100644 bin/main/net/thauvin/erik/mobibot/entries/FeedsManager.kt create mode 100644 bin/main/net/thauvin/erik/mobibot/modules/AbstractModule.kt create mode 100644 bin/main/net/thauvin/erik/mobibot/modules/Calc.kt create mode 100644 bin/main/net/thauvin/erik/mobibot/modules/ChatGpt.kt create mode 100644 bin/main/net/thauvin/erik/mobibot/modules/CryptoPrices.kt create mode 100644 bin/main/net/thauvin/erik/mobibot/modules/CurrencyConverter.kt create mode 100644 bin/main/net/thauvin/erik/mobibot/modules/Dice.kt create mode 100644 bin/main/net/thauvin/erik/mobibot/modules/GoogleSearch.kt create mode 100644 bin/main/net/thauvin/erik/mobibot/modules/Joke.kt create mode 100644 bin/main/net/thauvin/erik/mobibot/modules/Lookup.kt create mode 100644 bin/main/net/thauvin/erik/mobibot/modules/Mastodon.kt create mode 100644 bin/main/net/thauvin/erik/mobibot/modules/ModuleException.kt create mode 100644 bin/main/net/thauvin/erik/mobibot/modules/Ping.kt create mode 100644 bin/main/net/thauvin/erik/mobibot/modules/RockPaperScissors.kt create mode 100644 bin/main/net/thauvin/erik/mobibot/modules/StockQuote.kt create mode 100644 bin/main/net/thauvin/erik/mobibot/modules/War.class create mode 100644 bin/main/net/thauvin/erik/mobibot/modules/Weather2.kt create mode 100644 bin/main/net/thauvin/erik/mobibot/modules/WolframAlpha.kt create mode 100644 bin/main/net/thauvin/erik/mobibot/modules/WorldTime.kt create mode 100644 bin/main/net/thauvin/erik/mobibot/msg/ErrorMessage.kt create mode 100644 bin/main/net/thauvin/erik/mobibot/msg/Message.kt create mode 100644 bin/main/net/thauvin/erik/mobibot/msg/NoticeMessage.kt create mode 100644 bin/main/net/thauvin/erik/mobibot/msg/PrivateMessage.kt create mode 100644 bin/main/net/thauvin/erik/mobibot/msg/PublicMessage.kt create mode 100644 bin/main/net/thauvin/erik/mobibot/social/SocialManager.kt create mode 100644 bin/main/net/thauvin/erik/mobibot/social/SocialModule.kt create mode 100644 bin/main/net/thauvin/erik/mobibot/social/SocialTimer.kt create mode 100644 bin/test/current.xml create mode 100644 bin/test/net/thauvin/erik/mobibot/AddonsTest.kt create mode 100644 bin/test/net/thauvin/erik/mobibot/ExceptionSanitizer.kt create mode 100644 bin/test/net/thauvin/erik/mobibot/FeedReaderTest.kt create mode 100644 bin/test/net/thauvin/erik/mobibot/LocalProperties.kt create mode 100644 bin/test/net/thauvin/erik/mobibot/PinboardTest.kt create mode 100644 bin/test/net/thauvin/erik/mobibot/UtilsTest.kt create mode 100644 bin/test/net/thauvin/erik/mobibot/commands/InfoTest.kt create mode 100644 bin/test/net/thauvin/erik/mobibot/commands/RecapTest.kt create mode 100644 bin/test/net/thauvin/erik/mobibot/commands/links/LinksManagerTest.kt create mode 100644 bin/test/net/thauvin/erik/mobibot/commands/links/ViewTest.kt create mode 100644 bin/test/net/thauvin/erik/mobibot/commands/seen/SeenTest.kt create mode 100644 bin/test/net/thauvin/erik/mobibot/commands/tell/TellMessageTest.kt create mode 100644 bin/test/net/thauvin/erik/mobibot/commands/tell/TellMessagesMgrTest.kt create mode 100644 bin/test/net/thauvin/erik/mobibot/entries/EntriesUtilsTest.kt create mode 100644 bin/test/net/thauvin/erik/mobibot/entries/EntryLinkTest.kt create mode 100644 bin/test/net/thauvin/erik/mobibot/entries/FeedMgrTest.kt create mode 100644 bin/test/net/thauvin/erik/mobibot/modules/CalcTest.kt create mode 100644 bin/test/net/thauvin/erik/mobibot/modules/ChatGptTest.kt create mode 100644 bin/test/net/thauvin/erik/mobibot/modules/CryptoPricesTest.kt create mode 100644 bin/test/net/thauvin/erik/mobibot/modules/CurrencyConverterTest.kt create mode 100644 bin/test/net/thauvin/erik/mobibot/modules/DiceTest.kt create mode 100644 bin/test/net/thauvin/erik/mobibot/modules/GoogleSearchTest.kt create mode 100644 bin/test/net/thauvin/erik/mobibot/modules/JokeTest.kt create mode 100644 bin/test/net/thauvin/erik/mobibot/modules/LookupTest.kt create mode 100644 bin/test/net/thauvin/erik/mobibot/modules/MastodonTest.kt create mode 100644 bin/test/net/thauvin/erik/mobibot/modules/ModuleExceptionTest.kt create mode 100644 bin/test/net/thauvin/erik/mobibot/modules/PingTest.kt create mode 100644 bin/test/net/thauvin/erik/mobibot/modules/RockPaperScissorsTest.kt create mode 100644 bin/test/net/thauvin/erik/mobibot/modules/StockQuoteTest.kt create mode 100644 bin/test/net/thauvin/erik/mobibot/modules/Weather2Test.kt create mode 100644 bin/test/net/thauvin/erik/mobibot/modules/WolframAlphaTest.kt create mode 100644 bin/test/net/thauvin/erik/mobibot/modules/WordTimeTest.kt create mode 100644 bin/test/net/thauvin/erik/mobibot/msg/MessageTest.kt create mode 100755 bld create mode 100644 bld.bat delete mode 100644 build.gradle delete mode 100644 gradle.properties delete mode 100644 gradle/wrapper/gradle-wrapper.jar delete mode 100644 gradle/wrapper/gradle-wrapper.properties delete mode 100755 gradlew delete mode 100644 gradlew.bat create mode 100644 lib/bld/bld-wrapper.jar create mode 100644 lib/bld/bld-wrapper.properties create mode 100644 release_info.txt delete mode 100644 settings.gradle create mode 100644 sonar-project.properties create mode 100644 src/bld/java/net/thauvin/erik/MobibotBuild.java delete mode 100644 src/main/java/net/thauvin/erik/mobibot/modules/War.java create mode 100644 src/main/kotlin/net/thauvin/erik/mobibot/ReleaseInfo.kt create mode 100644 src/main/kotlin/net/thauvin/erik/mobibot/modules/War.kt create mode 100644 src/main/resources/log4j2.xml create mode 100644 src/test/kotlin/net/thauvin/erik/mobibot/DisableOnCi.kt create mode 100644 src/test/kotlin/net/thauvin/erik/mobibot/DisableOnCiCondition.kt delete mode 100644 version.mustache delete mode 100644 version.properties diff --git a/.circleci/config.yml b/.circleci/config.yml index 1868b37..77889be 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -6,49 +6,39 @@ defaults: &defaults TERM: dumb CI_NAME: "CircleCI" -defaults_gradle: &defaults_gradle +defaults_gradle: &defaults_bld steps: - checkout - - restore_cache: - keys: - - gradle-dependencies-{{ checksum "build.gradle" }} - # fallback to using the latest cache if no exact match is found - - gradle-dependencies- - run: - name: Gradle Dependencies - command: ./gradlew dependencies - - save_cache: - paths: - - ~/.m2 - key: gradle-dependencies-{{ checksum "build.gradle" }} + name: Download the bld dependencies + command: ./bld download - run: - name: Run All Checks - command: ./gradlew check - - store_artifacts: - path: build/reports/ - destination: reports - - store_test_results: - path: build/reports/ + name: Compile source with bld + command: ./bld compile + - run: + name: Run tests with bld + command: ./bld test jobs: - build_gradle_jdk17: + bld_jdk17: <<: *defaults docker: - image: cimg/openjdk:17.0 - <<: *defaults_gradle + <<: *defaults_bld - build_gradle_jdk19: + bld_jdk20: <<: *defaults docker: - - image: cimg/openjdk:19.0 + - image: cimg/openjdk:20.0 - <<: *defaults_gradle + <<: *defaults_bld workflows: version: 2 - gradle: + bld: jobs: - - build_gradle_jdk17 + - bld_jdk17 + - bld_jdk20 diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index aa034f9..ae6c66f 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -1,21 +1,21 @@ -name: gradle-ci +name: bld-ci on: [ push, pull_request, workflow_dispatch ] jobs: - build: + build-bld-project: runs-on: ubuntu-latest env: - GRADLE_OPTS: "-Dorg.gradle.jvmargs=-XX:MaxMetaspaceSize=512m" - SONAR_JDK: "17" + COVERAGE_SDK: "17" strategy: matrix: java-version: [ 17, 20 ] steps: - - uses: actions/checkout@v3 + - name: Checkout source repository + uses: actions/checkout@v3 with: fetch-depth: 0 @@ -25,35 +25,21 @@ jobs: distribution: 'zulu' java-version: ${{ matrix.java-version }} - - name: Grant execute permission for gradlew - run: chmod +x gradlew + - name: Grant bld execute permission + run: chmod +x bld - - name: Cache SonarCloud packages - if: matrix.java-version == env.SONAR_JDK - uses: actions/cache@v3 - with: - path: ~/.sonar/cache - key: ${{ runner.os }}-sonar - restore-keys: ${{ runner.os }}-sonar + - name: Download the bld dependencies + run: ./bld download - - name: Test with Gradle - uses: gradle/gradle-build-action@v2 - env: - CI_NAME: "GitHub CI" - ALPHAVANTAGE_API_KEY: ${{ secrets.ALPHAVANTAGE_API_KEY }} - CHATGPT_API_KEY: ${{ secrets.CHATGPT_API_KEY }} - OWM_API_KEY: ${{ secrets.OWM_API_KEY }} - PINBOARD_API_TOKEN: ${{ secrets.PINBOARD_API_TOKEN }} - MASTODON_ACCESS_TOKEN: ${{ secrets.MASTODON_ACCESS_TOKEN }} - MASTODON_HANDLE: ${{ secrets.MASTODON_HANDLE }} - MASTODON_INSTANCE: ${{ secrets.MASTODON_INSTANCE }} - EXCHANGERATE_API_KEY: ${{ secrets.EXCHANGERATE_API_KEY }} - with: - arguments: build check --stacktrace + - name: Compile source with bld + run: ./bld compile - - name: SonarCloud - if: success() && matrix.java-version == env.SONAR_JDK + - name: Run tests with bld + run: ./bld jacoco + + - name: SonarCloud Scan + uses: sonarsource/sonarcloud-github-action@master + if: success() && matrix.java-version == env.COVERAGE_SDK env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - run: ./gradlew sonar --info diff --git a/.gitignore b/.gitignore index 970cefd..fa550e5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,89 +1,61 @@ -!.vscode/extensions.json -!.vscode/launch.json -!.vscode/settings.json -!.vscode/tasks.json -!gradle-wrapper.jar -!properties/* -*.class -*.code-workspace -*.ctxt -*.ear -*.iws -*.jar -*.log -*.nar -*.rar -*.sublime-* -*.tar.gz -*.war -*.zip -.DS_Store -.classpath -.gradle -.history -.idea/**/caches/build_file_checksums.ser -.idea/**/contentModel.xml -.idea/**/dataSources.ids -.idea/**/dataSources.local.xml -.idea/**/dataSources/ -.idea/**/dbnavigator.xml -.idea/**/dictionaries -.idea/**/dynamic.xml -.idea/**/gradle.xml -.idea/**/httpRequests -.idea/**/libraries -.idea/**/mongoSettings.xml -.idea/**/replstate.xml -.idea/**/shelf -.idea/**/shelf/ -.idea/**/sonarlint* -.idea/**/sqlDataSources.xml -.idea/**/tasks.xml -.idea/**/uiDesigner.xml -.idea/**/usage.statistics.xml -.idea/**/workspace.xml -.idea_modules/ -.kobalt -.mtj.tmp/ -.mvn/timing.properties -.mvn/wrapper/maven-wrapper.jar -.nb-gradle -.project -.scannerwork -.settings -.vscode/* -Thumbs.db -__pycache__ -atlassian-ide-plugin.xml -bin/ -build/ -cmake-build-*/ -com_crashlytics_export_strings.xml -crashlytics-build.properties -crashlytics.properties -dependency-reduced-pom.xml -deploy/ -dist/ -ehthumbs.db -fabric.properties -gen/ -hs_err_pid* -kobaltBuild -kobaltw*-test -lib/kotlin* -libs/ -local.properties -log4j2.xml -logs/ -mobibot.properties -out/ -pom.xml.next -pom.xml.releaseBackup -pom.xml.tag -pom.xml.versionsBackup -proguard-project.txt -project.properties -release.properties -target/ -test-output -venv +.gradle +.DS_Store +build +lib/bld/** +!lib/bld/bld-wrapper.jar +!lib/bld/bld-wrapper.properties +lib/compile/ +lib/runtime/ +lib/standalone/ +lib/test/ + +# IDEA ignores + +# User-specific +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Editor-based Rest Client +.idea/httpRequests + +local.properties + +deploy +logs +mobibot.properties diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 48a3396..052df48 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,32 +1,11 @@ -image: gradle:8-jdk17 - -variables: - GRADLE_OPTS: "-Dorg.gradle.daemon=false" - CI_NAME: "GitLab CI" - -before_script: - - export GRADLE_USER_HOME=`pwd`/.gradle +image: openjdk:17 stages: - - build - test -build: - stage: build - script: gradle --build-cache assemble - cache: - key: "$CI_COMMIT_REF_NAME" - policy: push - paths: - - build - - .gradle - test: stage: test - script: gradle check - cache: - key: "$CI_COMMIT_REF_NAME" - policy: pull - paths: - - build - - .gradle + script: + - ./bld download + - ./bld compile + - ./bld test diff --git a/.idea/app.iml b/.idea/app.iml new file mode 100644 index 0000000..2c1fe21 --- /dev/null +++ b/.idea/app.iml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/bld.iml b/.idea/bld.iml new file mode 100644 index 0000000..e63e11e --- /dev/null +++ b/.idea/bld.iml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml deleted file mode 100644 index 10aa334..0000000 --- a/.idea/codeStyles/Project.xml +++ /dev/null @@ -1,298 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml deleted file mode 100644 index 23f4bb5..0000000 --- a/.idea/codeStyles/codeStyleConfig.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml deleted file mode 100644 index b589d56..0000000 --- a/.idea/compiler.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/copyright/Erik_s_Copyright_Notice.xml b/.idea/copyright/BSD_3.xml similarity index 93% rename from .idea/copyright/Erik_s_Copyright_Notice.xml rename to .idea/copyright/BSD_3.xml index 055999a..275eca9 100644 --- a/.idea/copyright/Erik_s_Copyright_Notice.xml +++ b/.idea/copyright/BSD_3.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml index 1419e40..3203074 100644 --- a/.idea/copyright/profiles_settings.xml +++ b/.idea/copyright/profiles_settings.xml @@ -1,3 +1,3 @@ - + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index 60682bf..1e01b48 100644 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -1,72 +1,8 @@ \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml deleted file mode 100644 index 0e29f96..0000000 --- a/.idea/jarRepositories.xml +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml deleted file mode 100644 index e805548..0000000 --- a/.idea/kotlinc.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/libraries/bld.xml b/.idea/libraries/bld.xml new file mode 100644 index 0000000..cf75013 --- /dev/null +++ b/.idea/libraries/bld.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/compile.xml b/.idea/libraries/compile.xml new file mode 100644 index 0000000..9bd86aa --- /dev/null +++ b/.idea/libraries/compile.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/runtime.xml b/.idea/libraries/runtime.xml new file mode 100644 index 0000000..2ae5c4b --- /dev/null +++ b/.idea/libraries/runtime.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/test.xml b/.idea/libraries/test.xml new file mode 100644 index 0000000..b80486a --- /dev/null +++ b/.idea/libraries/test.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index a59e398..e853f87 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,16 +1,14 @@ + - - - + + + + - - + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..55adcb9 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations/Run Tests.xml b/.idea/runConfigurations/Run Tests.xml new file mode 100644 index 0000000..37dc742 --- /dev/null +++ b/.idea/runConfigurations/Run Tests.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..c6500f2 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,11 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "java", + "name": "Run Tests", + "request": "launch", + "mainClass": "net.thauvin.erik.MobibotTest" + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..133aa45 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,15 @@ +{ + "java.project.sourcePaths": [ + "src/main/java", + "src/main/resources", + "src/test/java", + "src/bld/java" + ], + "java.configuration.updateBuildConfiguration": "automatic", + "java.project.referencedLibraries": [ + "${HOME}/.bld/dist/bld-1.7.5.jar", + "lib/compile/*.jar", + "lib/runtime/*.jar", + "lib/test/*.jar" + ] +} diff --git a/README.md b/README.md index 1ecefeb..fbcacc2 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # mobibot [![License (3-Clause BSD)](https://img.shields.io/badge/license-BSD%203--Clause-blue.svg)](https://opensource.org/licenses/BSD-3-Clause) -[![Kotlin](https://img.shields.io/badge/kotlin-1.9.10-7f52ff.svg)](https://kotlinlang.org) +[![Kotlin](https://img.shields.io/badge/kotlin-1.9.20-7f52ff.svg)](https://kotlinlang.org) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=ethauvin_mobibot&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=ethauvin_mobibot) [![GitHub CI](https://github.com/ethauvin/mobibot/actions/workflows/gradle.yml/badge.svg)](https://github.com/ethauvin/mobibot/actions/workflows/gradle.yml) [![CircleCI](https://circleci.com/gh/ethauvin/mobibot/tree/master.svg?style=shield)](https://circleci.com/gh/ethauvin/mobibot/tree/master) @@ -14,8 +14,8 @@ Some very basic instructions: cd mobibot - # build with gradle - ./gradlew + # build JAR and deploy + ./bld jar deploy cd deploy diff --git a/bin/main/log4j2.xml b/bin/main/log4j2.xml new file mode 100644 index 0000000..265c88f --- /dev/null +++ b/bin/main/log4j2.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bin/main/net/thauvin/erik/mobibot/Addons.kt b/bin/main/net/thauvin/erik/mobibot/Addons.kt new file mode 100644 index 0000000..2c5f05d --- /dev/null +++ b/bin/main/net/thauvin/erik/mobibot/Addons.kt @@ -0,0 +1,190 @@ +/* + * Addons.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.thauvin.erik.mobibot + +import net.thauvin.erik.mobibot.Utils.notContains +import net.thauvin.erik.mobibot.commands.AbstractCommand +import net.thauvin.erik.mobibot.commands.links.LinksManager +import net.thauvin.erik.mobibot.modules.AbstractModule +import org.pircbotx.hooks.events.PrivateMessageEvent +import org.pircbotx.hooks.types.GenericMessageEvent +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.util.* + +/** + * Modules and Commands addons. + */ +class Addons(private val props: Properties) { + private val logger: Logger = LoggerFactory.getLogger(Addons::class.java) + private val disabledModules = props.getProperty("disabled-modules", "").split(LinksManager.TAG_MATCH) + private val disableCommands = props.getProperty("disabled-commands", "").split(LinksManager.TAG_MATCH) + + val commands: MutableList = mutableListOf() + val modules: MutableList = mutableListOf() + val names = Names + + /** + * Add a module with properties. + */ + fun add(module: AbstractModule): Boolean { + var enabled = false + with(module) { + if (disabledModules.notContains(name, true)) { + if (hasProperties()) { + propertyKeys.forEach { + setProperty(it, props.getProperty(it, "")) + } + } + + if (isEnabled) { + modules.add(this) + names.modules.add(name) + names.commands.addAll(commands) + enabled = true + } else { + if (logger.isDebugEnabled) { + logger.debug("Module $name is disabled.") + } + names.disabledModules.add(name) + } + } else { + names.disabledModules.add(name) + } + } + return enabled + } + + /** + * Add a command with properties. + */ + fun add(command: AbstractCommand): Boolean { + var enabled = false + with(command) { + if (disableCommands.notContains(name, true)) { + if (properties.isNotEmpty()) { + properties.keys.forEach { + setProperty(it, props.getProperty(it, "")) + } + } + if (isEnabled()) { + commands.add(this) + if (isVisible) { + if (isOpOnly) { + names.ops.add(name) + } else { + names.commands.add(name) + } + } + enabled = true + } else { + if (logger.isDebugEnabled) { + logger.debug("Command $name is disabled.") + } + names.disabledCommands.add(name) + } + } else { + names.disabledCommands.add(name) + } + } + return enabled + } + + /** + * Execute a command or module. + */ + fun exec(channel: String, cmd: String, args: String, event: GenericMessageEvent): Boolean { + val cmds = if (event is PrivateMessageEvent) commands else commands.filter { it.isPublic } + for (command in cmds) { + if (command.name.startsWith(cmd)) { + command.commandResponse(channel, args, event) + return true + } + } + val mods = if (event is PrivateMessageEvent) modules.filter { it.isPrivateMsgEnabled } else modules + for (module in mods) { + if (module.commands.contains(cmd)) { + module.commandResponse(channel, cmd, args, event) + return true + } + } + return false + } + + /** + * Match a command. + */ + fun match(channel: String, event: GenericMessageEvent): Boolean { + for (command in commands) { + if (command.matches(event.message)) { + command.commandResponse(channel, event.message, event) + return true + } + } + return false + } + + /** + * Commands and Modules help. + */ + fun help(channel: String, topic: String, event: GenericMessageEvent): Boolean { + for (command in commands) { + if (command.isVisible && command.name.startsWith(topic)) { + return command.helpResponse(channel, topic, event) + } + } + for (module in modules) { + if (module.commands.contains(topic)) { + return module.helpResponse(event) + } + } + return false + } + + /** + * Holds commands and modules names. + */ + object Names { + val modules: MutableList = mutableListOf() + val disabledModules: MutableList = mutableListOf() + val commands: MutableList = mutableListOf() + val disabledCommands: MutableList = mutableListOf() + val ops: MutableList = mutableListOf() + + fun sort() { + modules.sort() + disabledModules.sort() + commands.sort() + disabledCommands.sort() + ops.sort() + } + } +} diff --git a/bin/main/net/thauvin/erik/mobibot/Constants.kt b/bin/main/net/thauvin/erik/mobibot/Constants.kt new file mode 100644 index 0000000..98ef74a --- /dev/null +++ b/bin/main/net/thauvin/erik/mobibot/Constants.kt @@ -0,0 +1,102 @@ +/* + * Constants.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.thauvin.erik.mobibot + +/** + * The `Constants`. + */ +object Constants { + /** + * The connect/read timeout in ms. + */ + const val CONNECT_TIMEOUT = 5000 + + /** + * Debug command line argument. + */ + const val DEBUG_ARG = "debug" + + /** + * Default IRC Port. + */ + const val DEFAULT_PORT = 6667 + + /** + * Default IRC Server. + */ + const val DEFAULT_SERVER = "irc.libera.chat" + + /** + * CLI command for usage. + */ + const val CLI_CMD = "java -jar ${ReleaseInfo.PROJECT}.jar" + + /** + * User-Agent + */ + const val USER_AGENT = + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36" + + /** + * The help command. + */ + const val HELP_CMD = "help" + + /** + * The link command. + */ + const val LINK_CMD = "L" + + /** + * The empty title string. + */ + const val NO_TITLE = "No Title" + + /** + * Properties command line argument. + */ + const val PROPS_ARG = "properties" + + /** + * The tag command + */ + const val TAG_CMD = "T" + + /** + * The timer delay in minutes. + */ + const val TIMER_DELAY = 10L + + /** + * Properties version line argument. + */ + const val VERSION_ARG = "version" +} diff --git a/bin/main/net/thauvin/erik/mobibot/FeedReader.kt b/bin/main/net/thauvin/erik/mobibot/FeedReader.kt new file mode 100644 index 0000000..d82f011 --- /dev/null +++ b/bin/main/net/thauvin/erik/mobibot/FeedReader.kt @@ -0,0 +1,92 @@ +/* + * FeedReader.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.thauvin.erik.mobibot + +import com.rometools.rome.io.FeedException +import com.rometools.rome.io.SyndFeedInput +import com.rometools.rome.io.XmlReader +import net.thauvin.erik.mobibot.Utils.green +import net.thauvin.erik.mobibot.Utils.helpFormat +import net.thauvin.erik.mobibot.Utils.sendMessage +import net.thauvin.erik.mobibot.entries.FeedsManager +import net.thauvin.erik.mobibot.msg.Message +import net.thauvin.erik.mobibot.msg.NoticeMessage +import org.pircbotx.hooks.types.GenericMessageEvent +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.io.IOException +import java.net.URL + +/** + * Reads an RSS feed. + */ +class FeedReader(private val url: String, val event: GenericMessageEvent) : Runnable { + private val logger: Logger = LoggerFactory.getLogger(FeedsManager::class.java) + + /** + * Fetches the Feed's items. + */ + override fun run() { + try { + readFeed(url).forEach { + event.sendMessage("", it) + } + } catch (e: FeedException) { + if (logger.isWarnEnabled) logger.warn("Unable to parse the feed at $url", e) + event.sendMessage("An error has occurred while parsing the feed: ${e.message}") + } catch (e: IOException) { + if (logger.isWarnEnabled) logger.warn("Unable to fetch the feed at $url", e) + event.sendMessage("An IO error has occurred while fetching the feed: ${e.message}") + } + } + + companion object { + @JvmStatic + @Throws(FeedException::class, IOException::class) + fun readFeed(url: String, maxItems: Int = 5): List { + val messages = mutableListOf() + val input = SyndFeedInput() + XmlReader(URL(url).openStream()).use { reader -> + val feed = input.build(reader) + val items = feed.entries + if (items.isEmpty()) { + messages.add(NoticeMessage("There is currently nothing to view.")) + } else { + items.take(maxItems).forEach { + messages.add(NoticeMessage(it.title)) + messages.add(NoticeMessage(helpFormat(it.link.green(), false))) + } + } + } + return messages + } + } +} diff --git a/bin/main/net/thauvin/erik/mobibot/Mobibot.kt b/bin/main/net/thauvin/erik/mobibot/Mobibot.kt new file mode 100644 index 0000000..f91c457 --- /dev/null +++ b/bin/main/net/thauvin/erik/mobibot/Mobibot.kt @@ -0,0 +1,421 @@ +/* + * Mobibot.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package net.thauvin.erik.mobibot + +import kotlinx.cli.ArgParser +import kotlinx.cli.ArgType +import kotlinx.cli.default +import net.thauvin.erik.mobibot.Utils.appendIfMissing +import net.thauvin.erik.mobibot.Utils.bot +import net.thauvin.erik.mobibot.Utils.capitalise +import net.thauvin.erik.mobibot.Utils.getIntProperty +import net.thauvin.erik.mobibot.Utils.helpCmdSyntax +import net.thauvin.erik.mobibot.Utils.helpFormat +import net.thauvin.erik.mobibot.Utils.isChannelOp +import net.thauvin.erik.mobibot.Utils.lastOrEmpty +import net.thauvin.erik.mobibot.Utils.sendList +import net.thauvin.erik.mobibot.Utils.sendMessage +import net.thauvin.erik.mobibot.Utils.toIsoLocalDate +import net.thauvin.erik.mobibot.commands.* +import net.thauvin.erik.mobibot.commands.Recap.Companion.storeRecap +import net.thauvin.erik.mobibot.commands.links.* +import net.thauvin.erik.mobibot.commands.seen.Seen +import net.thauvin.erik.mobibot.commands.tell.Tell +import net.thauvin.erik.mobibot.modules.* +import net.thauvin.erik.semver.Version +import org.pircbotx.Configuration +import org.pircbotx.PircBotX +import org.pircbotx.hooks.ListenerAdapter +import org.pircbotx.hooks.events.* +import org.pircbotx.hooks.types.GenericMessageEvent +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.io.* +import java.nio.file.Files +import java.nio.file.Paths +import java.util.* +import java.util.regex.Pattern +import kotlin.system.exitProcess + +@Version(properties = "version.properties", className = "ReleaseInfo", template = "version.mustache", type = "kt") +class Mobibot(nickname: String, val channel: String, logsDirPath: String, p: Properties) : ListenerAdapter() { + // The bot configuration. + private val config: Configuration + + // Commands and Modules + private val addons: Addons + + // Seen command + private val seen: Seen + + // Tell command + private val tell: Tell + + /** Logger. */ + val logger: Logger = LoggerFactory.getLogger(Mobibot::class.java) + + /** + * Connects to the server and joins the channel. + */ + fun connect() { + PircBotX(config).startBot() + } + + /** + * Responds with the default help. + */ + private fun helpDefault(event: GenericMessageEvent) { + event.sendMessage("Type a URL on $channel to post it.") + event.sendMessage("For more information on a specific command, type:") + event.sendMessage( + helpFormat( + helpCmdSyntax("%c ${Constants.HELP_CMD} ", event.bot().nick, event is PrivateMessageEvent) + ) + ) + event.sendMessage("The commands are:") + event.sendList(addons.names.commands, 8, isBold = true, isIndent = true) + if (event.isChannelOp(channel)) { + if (addons.names.disabledCommands.isNotEmpty()) { + event.sendMessage("The disabled commands are:") + event.sendList(addons.names.disabledCommands, 8, isBold = false, isIndent = true) + } + event.sendMessage("The op commands are:") + event.sendList(addons.names.ops, 8, isBold = true, isIndent = true) + } + } + + /** + * Responds with the default, commands or modules help. + */ + private fun helpResponse(event: GenericMessageEvent, topic: String) { + if (topic.isBlank() || !addons.help(channel, topic.lowercase().trim(), event)) { + helpDefault(event) + } + } + + override fun onAction(event: ActionEvent?) { + event?.channel?.let { + if (channel == it.name) { + event.user?.let { user -> + storeRecap(user.nick, event.action, true) + } + } + } + } + + override fun onDisconnect(event: DisconnectEvent?) { + event?.let { + with(event.getBot()) { + LinksManager.socialManager.notification("$nick disconnected from $serverHostname") + seen.add(userChannelDao.getChannel(channel).users) + } + } + LinksManager.socialManager.shutdown() + } + + override fun onPrivateMessage(event: PrivateMessageEvent?) { + event?.user?.let { user -> + if (logger.isTraceEnabled) logger.trace("<<< ${user.nick}: ${event.message}") + val cmds = event.message.trim().split(" ".toRegex(), 2) + val cmd = cmds[0].lowercase() + val args = cmds.lastOrEmpty().trim() + if (cmd.startsWith(Constants.HELP_CMD)) { // help + helpResponse(event, args) + } else if (!addons.exec(channel, cmd, args, event)) { // Execute command or module + helpDefault(event) + } + } + } + + override fun onJoin(event: JoinEvent?) { + event?.user?.let { user -> + with(event.getBot()) { + if (user.nick == nick) { + LinksManager.socialManager.notification( + "$nick has joined ${event.channel.name} on $serverHostname" + ) + seen.add(userChannelDao.getChannel(channel).users) + } else { + tell.send(event) + seen.add(user.nick) + } + } + } + } + + override fun onMessage(event: MessageEvent?) { + event?.user?.let { user -> + tell.send(event) + if (event.message.matches("(?i)${Pattern.quote(event.bot().nick)}:.*".toRegex())) { // mobibot: + if (logger.isTraceEnabled) logger.trace(">>> ${user.nick}: ${event.message}") + val cmds = event.message.substring(event.bot().nick.length + 1).trim().split(" ".toRegex(), 2) + val cmd = cmds[0].lowercase() + val args = cmds.lastOrEmpty().trim() + if (cmd.startsWith(Constants.HELP_CMD)) { // mobibot: help + helpResponse(event, args) + } else { + // Execute module or command + addons.exec(channel, cmd, args, event) + } + } else if (addons.match(channel, event)) { // Links, e.g.: https://www.example.com/ or L1: , etc. + if (logger.isTraceEnabled) logger.trace(">>> ${user.nick}: ${event.message}") + } + storeRecap(user.nick, event.message, false) + seen.add(user.nick) + } + } + + override fun onNickChange(event: NickChangeEvent?) { + event?.let { + tell.send(event) + if (!it.oldNick.equals(it.newNick, true)) { + seen.add(it.oldNick) + } + seen.add(it.newNick) + } + } + + override fun onPart(event: PartEvent?) { + event?.user?.let { user -> + with(event.getBot()) { + if (user.nick == nick) { + LinksManager.socialManager.notification( + "$nick has left ${event.channel.name} on $serverHostname" + ) + seen.add(userChannelDao.getChannel(channel).users) + } else { + seen.add(user.nick) + } + } + } + } + + override fun onQuit(event: QuitEvent?) { + event?.user?.let { user -> + seen.add(user.nick) + } + } + + companion object { + @JvmStatic + @Throws(Exception::class) + fun main(args: Array) { + // Set up the command line options + val parser = ArgParser(Constants.CLI_CMD) + val debug by parser.option( + ArgType.Boolean, + Constants.DEBUG_ARG, + Constants.DEBUG_ARG.substring(0, 1), + "Print debug & logging data directly to the console" + ).default(false) + val property by parser.option( + ArgType.String, + Constants.PROPS_ARG, + Constants.PROPS_ARG.substring(0, 1), + "Use alternate properties file" + ).default("./${ReleaseInfo.PROJECT}.properties") + val version by parser.option( + ArgType.Boolean, + Constants.VERSION_ARG, + Constants.VERSION_ARG.substring(0, 1), + "Print version info" + ).default(false) + + // Parse the command line + parser.parse(args) + + if (version) { + // Output the version + println( + "${ReleaseInfo.PROJECT.capitalise()} ${ReleaseInfo.VERSION}" + + " (${ReleaseInfo.BUILDDATE.toIsoLocalDate()})" + ) + println(ReleaseInfo.WEBSITE) + } else { + // Load the properties + val p = Properties() + try { + Files.newInputStream( + Paths.get(property) + ).use { fis -> + p.load(fis) + } + } catch (ignore: FileNotFoundException) { + System.err.println("Unable to find properties file.") + exitProcess(1) + } catch (ignore: IOException) { + System.err.println("Unable to open properties file.") + exitProcess(1) + } + val nickname = p.getProperty("nick", Mobibot::class.java.name.lowercase()) + val channel = p.getProperty("channel") + val logsDir = p.getProperty("logs", ".").appendIfMissing(File.separatorChar) + + // Redirect stdout and stderr + if (!debug) { + try { + val stdout = PrintStream( + BufferedOutputStream( + FileOutputStream( + logsDir + channel.substring(1) + '.' + Utils.today() + ".log", true + ) + ), true + ) + System.setOut(stdout) + } catch (ignore: IOException) { + System.err.println("Unable to open output (stdout) log file.") + exitProcess(1) + } + try { + val stderr = PrintStream( + BufferedOutputStream( + FileOutputStream("$logsDir$nickname.err", true) + ), true + ) + System.setErr(stderr) + } catch (ignore: IOException) { + System.err.println("Unable to open error (stderr) log file.") + exitProcess(1) + } + } + + // Start the bot + Mobibot(nickname, channel, logsDir, p).connect() + } + } + } + + /** + * Initialize the bot. + */ + init { + val ircServer = p.getProperty("server", Constants.DEFAULT_SERVER) + config = Configuration.Builder().apply { + name = nickname + login = p.getProperty("login", nickname) + realName = p.getProperty("realname", nickname) + addServer( + ircServer, + p.getIntProperty("port", Constants.DEFAULT_PORT) + ) + addAutoJoinChannel(channel) + addListener(this@Mobibot) + version = "${ReleaseInfo.PROJECT} ${ReleaseInfo.VERSION}" + isAutoNickChange = true + val identPwd = p.getProperty("ident") + if (!identPwd.isNullOrBlank()) { + nickservPassword = identPwd + } + val identNick = p.getProperty("ident-nick") + if (!identNick.isNullOrBlank()) { + nickservNick = identNick + } + val identMsg = p.getProperty("ident-msg") + if (!identMsg.isNullOrBlank()) { + nickservCustomMessage = identMsg + } + isAutoReconnect = true + + //socketConnectTimeout = Constants.CONNECT_TIMEOUT + //socketTimeout = Constants.CONNECT_TIMEOUT + //messageDelay = StaticDelay(500) + }.buildConfiguration() + + // Load the current entries + with(LinksManager) { + entries.channel = channel + entries.ircServer = ircServer + entries.logsDir = logsDirPath + entries.backlogs = p.getProperty("backlogs", "") + entries.load() + + // Set up pinboard + pinboard.setApiToken(p.getProperty("pinboard-api-token", "")) + } + + addons = Addons(p) + + // Load the commands + addons.add(ChannelFeed(channel.removePrefix("#"))) + addons.add(Comment()) + addons.add(Cycle()) + addons.add(Die()) + addons.add(Ignore()) + addons.add(LinksManager()) + addons.add(Me()) + addons.add(Modules(addons.names.modules, addons.names.disabledModules)) + addons.add(Msg()) + addons.add(Nick()) + addons.add(Posting()) + addons.add(Recap()) + addons.add(Say()) + + // Seen command + seen = Seen("${logsDirPath}${nickname}-seen.ser") + addons.add(seen) + + addons.add(Tags()) + + // Tell command + tell = Tell("${logsDirPath}${nickname}.ser") + addons.add(tell) + + addons.add(Users()) + addons.add(Versions()) + addons.add(View()) + + // Load social modules + LinksManager.socialManager.add(addons, Mastodon()) + + // Load the modules + addons.add(Calc()) + addons.add(ChatGpt()) + addons.add(CryptoPrices()) + addons.add(CurrencyConverter()) + addons.add(Dice()) + addons.add(GoogleSearch()) + addons.add(Info(tell, seen)) + addons.add(Joke()) + addons.add(Lookup()) + addons.add(Ping()) + addons.add(RockPaperScissors()) + addons.add(StockQuote()) + addons.add(War()) + addons.add(Weather2()) + addons.add(WolframAlpha()) + addons.add(WorldTime()) + + // Sort the addons + addons.names.sort() + } +} + diff --git a/bin/main/net/thauvin/erik/mobibot/Pinboard.kt b/bin/main/net/thauvin/erik/mobibot/Pinboard.kt new file mode 100644 index 0000000..7cb5aed --- /dev/null +++ b/bin/main/net/thauvin/erik/mobibot/Pinboard.kt @@ -0,0 +1,113 @@ +/* + * Pinboard.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package net.thauvin.erik.mobibot + +import net.thauvin.erik.mobibot.entries.EntryLink +import net.thauvin.erik.pinboard.PinboardPoster +import java.time.ZoneId +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter +import java.time.temporal.ChronoUnit +import java.util.* + +/** + * Handles posts to pinboard.in. + */ +class Pinboard { + private val poster = PinboardPoster() + + /** + * Adds a pin. + */ + fun addPin(ircServer: String, entry: EntryLink) { + if (poster.apiToken.isNotBlank()) { + with(entry) { + poster.addPin(link, title, postedBy(ircServer), formatTags(), date.toTimestamp()) + } + } + } + + /** + * Sets the pinboard API token. + */ + fun setApiToken(apiToken: String) { + poster.apiToken = apiToken + } + + /** + * Deletes a pin. + */ + fun deletePin(entry: EntryLink) { + if (poster.apiToken.isNotBlank()) { + poster.deletePin(entry.link) + } + + } + + /** + * Updates a pin. + */ + fun updatePin(ircServer: String, oldUrl: String, entry: EntryLink) { + if (poster.apiToken.isNotBlank()) { + with(entry) { + if (oldUrl != link) { + poster.deletePin(oldUrl) + } + poster.addPin(link, title, postedBy(ircServer), formatTags(), date.toTimestamp()) + } + } + } + + /** + * Formats a date to a UTC timestamp. + */ + private fun Date.toTimestamp(): String { + return ZonedDateTime.ofInstant( + toInstant().truncatedTo(ChronoUnit.SECONDS), ZoneId.systemDefault() + ).format(DateTimeFormatter.ISO_INSTANT) + } + + /** + * Formats the tags for pinboard. + */ + private fun EntryLink.formatTags(): String { + return nick + formatTags(",", ",") + } + + /** + * Returns the pinboard.in extended attribution line. + */ + private fun EntryLink.postedBy(ircServer: String): String { + return "Posted by $nick on $channel ( $ircServer )" + } +} + diff --git a/bin/main/net/thauvin/erik/mobibot/Utils.kt b/bin/main/net/thauvin/erik/mobibot/Utils.kt new file mode 100644 index 0000000..e4760d2 --- /dev/null +++ b/bin/main/net/thauvin/erik/mobibot/Utils.kt @@ -0,0 +1,439 @@ +/* + * Utils.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.thauvin.erik.mobibot + +import net.thauvin.erik.mobibot.msg.Message +import net.thauvin.erik.mobibot.msg.Message.Companion.DEFAULT_COLOR +import net.thauvin.erik.urlencoder.UrlEncoderUtil +import org.jsoup.Jsoup +import org.pircbotx.Colors +import org.pircbotx.PircBotX +import org.pircbotx.hooks.events.PrivateMessageEvent +import org.pircbotx.hooks.types.GenericMessageEvent +import org.slf4j.Logger +import java.io.* +import java.net.HttpURLConnection +import java.net.URL +import java.nio.file.Files +import java.nio.file.Paths +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.format.DateTimeFormatter +import java.util.* +import kotlin.io.path.exists +import kotlin.io.path.fileSize + +/** + * Miscellaneous utilities. + */ +@Suppress("TooManyFunctions") +object Utils { + private val searchFlags = arrayOf("%c", "%n") + + /** + * Prepends a prefix if not present. + */ + @JvmStatic + fun String.prefixIfMissing(prefix: Char): String { + return if (first() != prefix) { + "$prefix${this}" + } else { + this + } + } + + /** + * Appends a suffix to the end of the String if not present. + */ + @JvmStatic + fun String.appendIfMissing(suffix: Char): String { + return if (last() != suffix) { + "$this${suffix}" + } else { + this + } + } + + /** + * Makes the given int bold. + */ + @JvmStatic + fun Int.bold(): String = toString().bold() + + /** + * Makes the given long bold. + */ + @JvmStatic + fun Long.bold(): String = toString().bold() + + /** + * Makes the given string bold. + */ + @JvmStatic + fun String?.bold(): String = colorize(Colors.BOLD) + + /** + * Returns the [PircBotX] instance. + */ + fun GenericMessageEvent.bot(): PircBotX { + return getBot() as PircBotX + } + + /** + * Capitalize a string. + */ + @JvmStatic + fun String.capitalise(): String = lowercase().replaceFirstChar { it.uppercase() } + + /** + * Capitalize words + */ + @JvmStatic + fun String.capitalizeWords(): String = split(" ").joinToString(" ") { it.capitalise() } + + /** + * Colorize a string. + */ + @JvmStatic + fun String?.colorize(color: String): String { + return when { + isNullOrEmpty() -> { + "" + } + + color == DEFAULT_COLOR -> { + this + } + + Colors.BOLD == color || Colors.REVERSE == color -> { + color + this + color + } + + else -> { + color + this + Colors.NORMAL + } + } + } + + /** + * Makes the given string cyan. + */ + @JvmStatic + fun String?.cyan(): String = colorize(Colors.CYAN) + + /** + * URL encodes the given string. + */ + @JvmStatic + fun String.encodeUrl(): String = UrlEncoderUtil.encode(this) + + /** + * Returns a property as an int. + */ + @JvmStatic + fun Properties.getIntProperty(key: String, defaultValue: Int): Int { + return getProperty(key)?.toIntOrDefault(defaultValue) ?: defaultValue + } + + /** + * Makes the given string green. + */ + @JvmStatic + fun String?.green(): String = colorize(Colors.DARK_GREEN) + + /** + * Build a help command by replacing `%c` with the bot's pub/priv command, and `%n` with the bot's + * nick. + */ + @JvmStatic + fun helpCmdSyntax(text: String, botNick: String, isPrivate: Boolean): String { + val replace = arrayOf(if (isPrivate) "/msg $botNick" else "$botNick:", botNick) + return text.replaceEach(searchFlags, replace) + } + + /** + * Returns a formatted help string. + */ + @JvmStatic + @JvmOverloads + fun helpFormat(help: String, isBold: Boolean = true, isIndent: Boolean = true): String { + val s = if (isBold) help.bold() else help + return if (isIndent) s.prependIndent() else s + } + + /** + * Returns `true` if the specified user is an operator on the [channel]. + */ + @JvmStatic + fun GenericMessageEvent.isChannelOp(channel: String): Boolean { + return this.bot().userChannelDao.getChannel(channel).isOp(this.user) + } + + /** + * Returns `true` if a HTTP status code indicates a successful response. + */ + @JvmStatic + fun Int.isHttpSuccess() = this in 200..399 + + /** + * Returns the last item of a list of strings or empty if none. + */ + @JvmStatic + fun List.lastOrEmpty(): String { + return if (this.size >= 2) { + this.last() + } else + "" + } + + /** + * Load serial data from file. + */ + @JvmStatic + fun loadSerialData(file: String, default: Any, logger: Logger, description: String): Any { + val serialFile = Paths.get(file) + if (serialFile.exists() && serialFile.fileSize() > 0) { + try { + ObjectInputStream( + BufferedInputStream(Files.newInputStream(serialFile)) + ).use { input -> + if (logger.isDebugEnabled) logger.debug("Loading the ${description}.") + return input.readObject() + } + } catch (e: IOException) { + logger.error("An IO error occurred loading the ${description}.", e) + } catch (e: ClassNotFoundException) { + logger.error("An error occurred loading the ${description}.", e) + } + } + return default + } + + /** + * Returns `true` if the list does not contain the given string. + */ + @JvmStatic + fun List.notContains(text: String, ignoreCase: Boolean = false) = this.none { it.equals(text, ignoreCase) } + + /** + * Obfuscates the given string. + */ + @JvmStatic + fun String.obfuscate(): String { + return if (isNotBlank()) { + "x".repeat(length) + } else this + } + + /** + * Returns the plural form of a word, if count > 1. + */ + @JvmStatic + fun String.plural(count: Long): String { + return if (count > 1) "${this}s" else this + } + + /** + * Makes the given string red. + */ + @JvmStatic + fun String?.red(): String = colorize(Colors.RED) + + /** + * Replaces all occurrences of Strings within another String. + */ + @JvmStatic + fun String.replaceEach(search: Array, replace: Array): String { + var result = this + if (search.size == replace.size) { + search.forEachIndexed { i, s -> + result = result.replace(s, replace[i]) + } + } + return result + } + + /** + * Makes the given string reverse color. + */ + @JvmStatic + fun String?.reverseColor(): String = colorize(Colors.REVERSE) + + /** + * Save data + */ + @JvmStatic + fun saveSerialData(file: String, data: Any, logger: Logger, description: String) { + try { + BufferedOutputStream(Files.newOutputStream(Paths.get(file))).use { bos -> + ObjectOutputStream(bos).use { output -> + if (logger.isDebugEnabled) logger.debug("Saving the ${description}.") + output.writeObject(data) + } + } + } catch (e: IOException) { + logger.error("Unable to save the ${description}.", e) + } + } + + /** + * Send a formatted commands/modules, etc. list. + */ + @JvmStatic + @JvmOverloads + fun GenericMessageEvent.sendList( + list: List, + maxPerLine: Int, + separator: String = " ", + isBold: Boolean = false, + isIndent: Boolean = false + ) { + var i = 0 + while (i < list.size) { + sendMessage( + helpFormat( + list.subList(i, list.size.coerceAtMost(i + maxPerLine)).joinToString(separator, truncated = ""), + isBold, + isIndent + ), + ) + i += maxPerLine + } + } + + /** + * Sends a [message]. + */ + @JvmStatic + fun GenericMessageEvent.sendMessage(channel: String, message: Message) { + if (message.isNotice) { + bot().sendIRC().notice(user.nick, message.msg.colorize(message.color)) + } else if (message.isPrivate || this is PrivateMessageEvent || channel.isBlank()) { + respondPrivateMessage(message.msg.colorize(message.color)) + } else { + bot().sendIRC().message(channel, message.msg.colorize(message.color)) + } + } + + /** + * Sends a response as a private message or notice. + */ + @JvmStatic + fun GenericMessageEvent.sendMessage(message: String) { + if (this is PrivateMessageEvent) { + respondPrivateMessage(message) + } else { + bot().sendIRC().notice(user.nick, message) + } + } + + /** + * Returns today's date. + */ + @JvmStatic + fun today(): String = LocalDateTime.now().toIsoLocalDate() + + /** + * Converts a string to an int. + */ + @JvmStatic + fun String.toIntOrDefault(defaultValue: Int): Int { + return try { + toInt() + } catch (e: NumberFormatException) { + defaultValue + } + } + + /** + * Returns the specified date as an ISO local date string. + */ + @JvmStatic + fun Date.toIsoLocalDate(): String { + return LocalDateTime.ofInstant(toInstant(), ZoneId.systemDefault()).toIsoLocalDate() + } + + /** + * Returns the specified date as an ISO local date string. + */ + @JvmStatic + fun LocalDateTime.toIsoLocalDate(): String = format(DateTimeFormatter.ISO_LOCAL_DATE) + + /** + * Returns the specified date formatted as `yyyy-MM-dd HH:mm`. + */ + @JvmStatic + fun Date.toUtcDateTime(): String { + return LocalDateTime.ofInstant(toInstant(), ZoneId.systemDefault()).toUtcDateTime() + } + + /** + * Returns the specified date formatted as `yyyy-MM-dd HH:mm`. + */ + @JvmStatic + fun LocalDateTime.toUtcDateTime(): String = format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")) + + /** + * Makes the given string bold. + */ + @JvmStatic + fun String?.underline(): String = colorize(Colors.UNDERLINE) + + + /** + * Converts XML/XHTML entities to plain text. + */ + @JvmStatic + fun String.unescapeXml(): String = Jsoup.parse(this).text() + + /** + * Reads contents of a URL. + */ + @JvmStatic + @Throws(IOException::class) + fun URL.reader(): UrlReaderResponse { + val connection = this.openConnection() as HttpURLConnection + connection.setRequestProperty( + "User-Agent", + "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/109.0" + ) + return if (connection.responseCode.isHttpSuccess()) { + UrlReaderResponse(connection.responseCode, connection.inputStream.bufferedReader().use { it.readText() }) + } else { + UrlReaderResponse(connection.responseCode, connection.errorStream.bufferedReader().use { it.readText() }) + } + } + + /** + * Holds the [URL.reader] response code and body text. + */ + data class UrlReaderResponse(val responseCode: Int, val body: String) +} diff --git a/bin/main/net/thauvin/erik/mobibot/commands/AbstractCommand.kt b/bin/main/net/thauvin/erik/mobibot/commands/AbstractCommand.kt new file mode 100644 index 0000000..5f79472 --- /dev/null +++ b/bin/main/net/thauvin/erik/mobibot/commands/AbstractCommand.kt @@ -0,0 +1,79 @@ +/* + * AbstractCommand.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package net.thauvin.erik.mobibot.commands + +import net.thauvin.erik.mobibot.Utils.bot +import net.thauvin.erik.mobibot.Utils.helpCmdSyntax +import net.thauvin.erik.mobibot.Utils.isChannelOp +import net.thauvin.erik.mobibot.Utils.sendMessage +import org.pircbotx.hooks.events.PrivateMessageEvent +import org.pircbotx.hooks.types.GenericMessageEvent + +abstract class AbstractCommand { + abstract val name: String + abstract val help: List + abstract val isOpOnly: Boolean + abstract val isPublic: Boolean + abstract val isVisible: Boolean + + val properties: MutableMap = mutableMapOf() + + abstract fun commandResponse(channel: String, args: String, event: GenericMessageEvent) + + open fun helpResponse(channel: String, topic: String, event: GenericMessageEvent): Boolean { + if (!isOpOnly || isOpOnly == event.isChannelOp(channel)) { + for (h in help) { + event.sendMessage(helpCmdSyntax(h, event.bot().nick, event is PrivateMessageEvent || !isPublic)) + } + return true + } + return false + } + + open fun initProperties(vararg keys: String) { + keys.forEach { + properties[it] = "" + } + } + + open fun isEnabled(): Boolean { + return true + } + + open fun matches(message: String): Boolean { + return false + } + + open fun setProperty(key: String, value: String) { + properties[key] = value + } +} diff --git a/bin/main/net/thauvin/erik/mobibot/commands/ChannelFeed.kt b/bin/main/net/thauvin/erik/mobibot/commands/ChannelFeed.kt new file mode 100644 index 0000000..038e378 --- /dev/null +++ b/bin/main/net/thauvin/erik/mobibot/commands/ChannelFeed.kt @@ -0,0 +1,62 @@ +/* + * ChannelFeed.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package net.thauvin.erik.mobibot.commands + +import net.thauvin.erik.mobibot.FeedReader +import net.thauvin.erik.mobibot.Utils.helpFormat +import org.pircbotx.hooks.types.GenericMessageEvent + +class ChannelFeed(channel: String) : AbstractCommand() { + override val name = channel + override val help = listOf("To list the last 5 posts from the channel's weblog feed:", helpFormat("%c $channel")) + override val isOpOnly = false + override val isPublic = true + override val isVisible = true + + companion object { + const val FEED_PROP = "feed" + } + + init { + initProperties(FEED_PROP) + } + + override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) { + if (isEnabled()) { + properties[FEED_PROP]?.let { FeedReader(it, event).run() } + } + } + + override fun isEnabled(): Boolean { + return !properties[FEED_PROP].isNullOrBlank() + } +} diff --git a/bin/main/net/thauvin/erik/mobibot/commands/Cycle.kt b/bin/main/net/thauvin/erik/mobibot/commands/Cycle.kt new file mode 100644 index 0000000..9608ca8 --- /dev/null +++ b/bin/main/net/thauvin/erik/mobibot/commands/Cycle.kt @@ -0,0 +1,66 @@ +/* + * Cycle.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package net.thauvin.erik.mobibot.commands + +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import net.thauvin.erik.mobibot.Utils.bot +import net.thauvin.erik.mobibot.Utils.helpFormat +import net.thauvin.erik.mobibot.Utils.isChannelOp +import org.pircbotx.hooks.types.GenericMessageEvent + +class Cycle : AbstractCommand() { + private val wait = 10 + override val name = "cycle" + override val help = listOf("To have the bot leave the channel and come back:", helpFormat("%c $name")) + override val isOpOnly = true + override val isPublic = false + override val isVisible = true + + override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) { + with(event.bot()) { + if (event.isChannelOp(channel)) { + runBlocking { + launch { + sendIRC().message(channel, "${event.user.nick} asked me to leave. I'll be back!") + userChannelDao.getChannel(channel).send().part() + delay(wait * 1000L) + sendIRC().joinChannel(channel) + } + } + } else { + helpResponse(channel, args, event) + } + } + } +} diff --git a/bin/main/net/thauvin/erik/mobibot/commands/Die.kt b/bin/main/net/thauvin/erik/mobibot/commands/Die.kt new file mode 100644 index 0000000..f271bfa --- /dev/null +++ b/bin/main/net/thauvin/erik/mobibot/commands/Die.kt @@ -0,0 +1,62 @@ +/* + * Die.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package net.thauvin.erik.mobibot.commands + +import net.thauvin.erik.mobibot.Utils.bot +import net.thauvin.erik.mobibot.Utils.isChannelOp +import org.pircbotx.hooks.types.GenericMessageEvent + +class Die : AbstractCommand() { + override val name = "die" + override val help = emptyList() + override val isOpOnly = true + override val isPublic = false + override val isVisible = false + + override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) { + with(event.bot()) { + if (event.isChannelOp(channel) && (properties[DIE_PROP].isNullOrBlank() || args == properties[DIE_PROP])) { + sendIRC().message(channel, "${event.user?.nick} has just signed my death sentence.") + stopBotReconnect() + sendIRC().quitServer("The Bot is Out There!") + } + } + } + + companion object { + const val DIE_PROP = "die" + } + + init { + initProperties(DIE_PROP) + } +} diff --git a/bin/main/net/thauvin/erik/mobibot/commands/Ignore.kt b/bin/main/net/thauvin/erik/mobibot/commands/Ignore.kt new file mode 100644 index 0000000..d083c10 --- /dev/null +++ b/bin/main/net/thauvin/erik/mobibot/commands/Ignore.kt @@ -0,0 +1,147 @@ +/* + * Ignore.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package net.thauvin.erik.mobibot.commands + +import net.thauvin.erik.mobibot.Utils.bold +import net.thauvin.erik.mobibot.Utils.bot +import net.thauvin.erik.mobibot.Utils.helpCmdSyntax +import net.thauvin.erik.mobibot.Utils.helpFormat +import net.thauvin.erik.mobibot.Utils.isChannelOp +import net.thauvin.erik.mobibot.Utils.sendList +import net.thauvin.erik.mobibot.Utils.sendMessage +import net.thauvin.erik.mobibot.commands.links.LinksManager +import org.pircbotx.hooks.types.GenericMessageEvent + +class Ignore : AbstractCommand() { + private val me = "me" + + init { + initProperties(IGNORE_PROP) + } + + override val name = IGNORE_CMD + override val help = listOf( + "To ignore a link posted to the channel:", + helpFormat("https://www.foo.bar %n"), + "To check your ignore status:", + helpFormat("%c $name"), + "To toggle your ignore status:", + helpFormat("%c $name $me") + ) + private val helpOp = help.plus( + arrayOf("To add/remove nicks from the ignored list:", helpFormat("%c $name [ ...]")) + ) + + override val isOpOnly = false + override val isPublic = true + override val isVisible = true + + companion object { + const val IGNORE_CMD = "ignore" + const val IGNORE_PROP = IGNORE_CMD + private val ignored = mutableSetOf() + + @JvmStatic + fun isNotIgnored(nick: String): Boolean { + return !ignored.contains(nick.lowercase()) + } + } + + override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) { + val isMe = args.trim().equals(me, true) + if (isMe || !event.isChannelOp(channel)) { + val nick = event.user.nick.lowercase() + ignoreNick(nick, isMe, event) + } else { + ignoreOp(args, event) + } + } + + override fun helpResponse(channel: String, topic: String, event: GenericMessageEvent): Boolean { + return if (event.isChannelOp(channel)) { + for (h in helpOp) { + event.sendMessage(helpCmdSyntax(h, event.bot().nick, true)) + } + true + } else { + super.helpResponse(channel, topic, event) + } + } + + private fun ignoreNick(sender: String, isMe: Boolean, event: GenericMessageEvent) { + if (isMe) { + if (ignored.remove(sender)) { + event.sendMessage("You are no longer ignored.") + } else { + ignored.add(sender) + event.sendMessage("You are now ignored.") + } + } else { + if (ignored.contains(sender)) { + event.sendMessage("You are currently ignored.") + } else { + event.sendMessage("You are not currently ignored.") + } + } + } + + private fun ignoreOp(args: String, event: GenericMessageEvent) { + if (args.isNotEmpty()) { + val nicks = args.lowercase().split(" ") + for (nick in nicks) { + val ignore = if (me == nick) { + nick.lowercase() + } else { + nick + } + if (!ignored.remove(ignore)) { + ignored.add(ignore) + } + } + } + + if (ignored.isNotEmpty()) { + event.sendMessage("The following nicks are ignored:") + event.sendList(ignored.sorted(), 8, isIndent = true) + } else { + event.sendMessage("No one is currently ${"ignored".bold()}.") + } + } + + override fun setProperty(key: String, value: String) { + super.setProperty(key, value) + if (IGNORE_PROP == key) { + ignored.addAll(value.split(LinksManager.TAG_MATCH)) + } + } + +} diff --git a/bin/main/net/thauvin/erik/mobibot/commands/Info.kt b/bin/main/net/thauvin/erik/mobibot/commands/Info.kt new file mode 100644 index 0000000..ed0b6ef --- /dev/null +++ b/bin/main/net/thauvin/erik/mobibot/commands/Info.kt @@ -0,0 +1,124 @@ +/* + * Info.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.thauvin.erik.mobibot.commands + +import net.thauvin.erik.mobibot.ReleaseInfo +import net.thauvin.erik.mobibot.Utils.capitalise +import net.thauvin.erik.mobibot.Utils.green +import net.thauvin.erik.mobibot.Utils.helpFormat +import net.thauvin.erik.mobibot.Utils.isChannelOp +import net.thauvin.erik.mobibot.Utils.plural +import net.thauvin.erik.mobibot.Utils.sendList +import net.thauvin.erik.mobibot.Utils.sendMessage +import net.thauvin.erik.mobibot.commands.links.LinksManager +import net.thauvin.erik.mobibot.commands.seen.Seen +import net.thauvin.erik.mobibot.commands.tell.Tell +import org.pircbotx.hooks.types.GenericMessageEvent +import java.lang.management.ManagementFactory +import kotlin.time.DurationUnit +import kotlin.time.toDuration + +class Info(private val tell: Tell, private val seen: Seen) : AbstractCommand() { + private val allVersions = listOf( + "${ReleaseInfo.PROJECT.capitalise()} ${ReleaseInfo.VERSION} (${ReleaseInfo.WEBSITE.green()})", + "Written by ${ReleaseInfo.AUTHOR} (${ReleaseInfo.AUTHOR_URL.green()})" + ) + override val name = "info" + override val help = listOf("To view information about the bot:", helpFormat("%c $name")) + override val isOpOnly = false + override val isPublic = true + override val isVisible = true + + companion object { + /** + * Converts milliseconds to year month week day hour and minutes. + */ + @JvmStatic + fun Long.toUptime(): String { + this.toDuration(DurationUnit.MILLISECONDS).toComponents { wholeDays, hours, minutes, seconds, _ -> + val years = wholeDays / 365 + var days = wholeDays % 365 + val months = days / 30 + days %= 30 + val weeks = days / 7 + days %= 7 + + with(StringBuffer()) { + if (years > 0) { + append(years).append(" year".plural(years)).append(' ') + } + if (months > 0) { + append(months).append(" month".plural(months)).append(' ') + } + if (weeks > 0) { + append(weeks).append(" week".plural(weeks)).append(' ') + } + if (days > 0) { + append(days).append(" day".plural(days)).append(' ') + } + if (hours > 0) { + append(hours).append(" hour".plural(hours.toLong())).append(' ') + } + + if (minutes > 0) { + append(minutes).append(" minute".plural(minutes.toLong())) + } else { + append(seconds).append(" second".plural(seconds.toLong())) + } + + return toString() + } + } + } + } + + override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) { + event.sendList(allVersions, 1) + val info = StringBuilder() + info.append("Uptime: ") + .append(ManagementFactory.getRuntimeMXBean().uptime.toUptime()) + .append(" [Entries: ") + .append(LinksManager.entries.links.size) + if (seen.isEnabled()) { + info.append(", Seen: ").append(seen.count()) + } + if (event.isChannelOp(channel)) { + if (tell.isEnabled()) { + info.append(", Messages: ").append(tell.size()) + } + if (LinksManager.socialManager.entriesCount() > 0) { + info.append(", Social: ").append(LinksManager.socialManager.entriesCount()) + } + } + info.append(", Recap: ").append(Recap.recaps.size).append(']') + event.sendMessage(info.toString()) + } +} diff --git a/bin/main/net/thauvin/erik/mobibot/commands/Me.kt b/bin/main/net/thauvin/erik/mobibot/commands/Me.kt new file mode 100644 index 0000000..ec7823b --- /dev/null +++ b/bin/main/net/thauvin/erik/mobibot/commands/Me.kt @@ -0,0 +1,51 @@ +/* + * Me.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package net.thauvin.erik.mobibot.commands + +import net.thauvin.erik.mobibot.Utils.bot +import net.thauvin.erik.mobibot.Utils.helpFormat +import net.thauvin.erik.mobibot.Utils.isChannelOp +import org.pircbotx.hooks.types.GenericMessageEvent + +class Me : AbstractCommand() { + override val name = "me" + override val help = listOf("To have the bot perform an action:", helpFormat("%c $name ")) + override val isOpOnly = true + override val isPublic = false + override val isVisible = true + + override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) { + if (event.isChannelOp(channel)) { + event.bot().sendIRC().action(channel, args) + } + } +} diff --git a/bin/main/net/thauvin/erik/mobibot/commands/Modules.kt b/bin/main/net/thauvin/erik/mobibot/commands/Modules.kt new file mode 100644 index 0000000..b2293b0 --- /dev/null +++ b/bin/main/net/thauvin/erik/mobibot/commands/Modules.kt @@ -0,0 +1,63 @@ +/* + * Modules.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package net.thauvin.erik.mobibot.commands + +import net.thauvin.erik.mobibot.Utils.helpFormat +import net.thauvin.erik.mobibot.Utils.isChannelOp +import net.thauvin.erik.mobibot.Utils.sendList +import org.pircbotx.hooks.types.GenericMessageEvent + +class Modules(private val modules: List, private val disabledModules: List) : AbstractCommand() { + override val name = "modules" + override val help = listOf("To view a list of enabled/disabled modules:", helpFormat("%c $name")) + override val isOpOnly = true + override val isPublic = false + override val isVisible = true + + override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) { + if (event.isChannelOp(channel)) { + if (modules.isEmpty()) { + event.respondPrivateMessage("There are no enabled modules.") + } else { + event.respondPrivateMessage("The enabled modules are: ") + event.sendList(modules, 7, isIndent = true) + } + if (disabledModules.isNotEmpty()) { + event.respondPrivateMessage("The disabled modules are: ") + event.sendList(disabledModules, 7, isIndent = true) + } + } else { + helpResponse(channel, args, event) + } + } +} + diff --git a/bin/main/net/thauvin/erik/mobibot/commands/Msg.kt b/bin/main/net/thauvin/erik/mobibot/commands/Msg.kt new file mode 100644 index 0000000..20a6635 --- /dev/null +++ b/bin/main/net/thauvin/erik/mobibot/commands/Msg.kt @@ -0,0 +1,60 @@ +/* + * Msg.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package net.thauvin.erik.mobibot.commands + +import net.thauvin.erik.mobibot.Utils.bot +import net.thauvin.erik.mobibot.Utils.helpFormat +import net.thauvin.erik.mobibot.Utils.isChannelOp +import org.pircbotx.hooks.types.GenericMessageEvent + +class Msg : AbstractCommand() { + override val name = "msg" + override val help = listOf( + "To have the bot send a private message to someone:", + helpFormat("%c $name ") + ) + override val isOpOnly = true + override val isPublic = false + override val isVisible = true + + override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) { + if (event.isChannelOp(channel)) { + val msg = args.split(" ", limit = 2) + if (args.length > 2) { + event.bot().sendIRC().message(msg[0], msg[1]) + event.respondPrivateMessage("A message was sent to ${msg[0]}") + } else { + helpResponse(channel, args, event) + } + } + } +} diff --git a/bin/main/net/thauvin/erik/mobibot/commands/Nick.kt b/bin/main/net/thauvin/erik/mobibot/commands/Nick.kt new file mode 100644 index 0000000..85a03ab --- /dev/null +++ b/bin/main/net/thauvin/erik/mobibot/commands/Nick.kt @@ -0,0 +1,51 @@ +/* + * Nick.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package net.thauvin.erik.mobibot.commands + +import net.thauvin.erik.mobibot.Utils.bot +import net.thauvin.erik.mobibot.Utils.helpFormat +import net.thauvin.erik.mobibot.Utils.isChannelOp +import org.pircbotx.hooks.types.GenericMessageEvent + +class Nick : AbstractCommand() { + override val name = "nick" + override val help = listOf("To change the bot's nickname:", helpFormat("%c $name ")) + override val isOpOnly = true + override val isPublic = true + override val isVisible = true + + override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) { + if (event.isChannelOp(channel)) { + event.bot().sendIRC().changeNick(args) + } + } +} diff --git a/bin/main/net/thauvin/erik/mobibot/commands/Recap.kt b/bin/main/net/thauvin/erik/mobibot/commands/Recap.kt new file mode 100644 index 0000000..77154c7 --- /dev/null +++ b/bin/main/net/thauvin/erik/mobibot/commands/Recap.kt @@ -0,0 +1,81 @@ +/* + * Recap.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package net.thauvin.erik.mobibot.commands + +import net.thauvin.erik.mobibot.Utils.helpFormat +import net.thauvin.erik.mobibot.Utils.sendMessage +import net.thauvin.erik.mobibot.Utils.toUtcDateTime +import org.pircbotx.hooks.types.GenericMessageEvent +import java.time.Clock +import java.time.LocalDateTime + +class Recap : AbstractCommand() { + override val name = "recap" + override val help = listOf( + "To list the last 10 public channel messages:", + helpFormat("%c $name") + ) + override val isOpOnly = false + override val isPublic = true + override val isVisible = true + + companion object { + const val MAX_RECAPS = 10 + + @JvmField + val recaps = mutableListOf() + + /** + * Stores the last 10 public messages and actions. + */ + @JvmStatic + fun storeRecap(sender: String, message: String, isAction: Boolean) { + recaps.add( + LocalDateTime.now(Clock.systemUTC()).toUtcDateTime() + + " - $sender" + (if (isAction) " " else ": ") + message + ) + if (recaps.size > MAX_RECAPS) { + recaps.removeFirst() + } + } + } + + override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) { + if (recaps.isNotEmpty()) { + for (r in recaps) { + event.sendMessage(r) + } + } else { + event.sendMessage("Sorry, nothing to recap.") + } + } +} diff --git a/bin/main/net/thauvin/erik/mobibot/commands/Say.kt b/bin/main/net/thauvin/erik/mobibot/commands/Say.kt new file mode 100644 index 0000000..7f76d35 --- /dev/null +++ b/bin/main/net/thauvin/erik/mobibot/commands/Say.kt @@ -0,0 +1,51 @@ +/* + * Say.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package net.thauvin.erik.mobibot.commands + +import net.thauvin.erik.mobibot.Utils.bot +import net.thauvin.erik.mobibot.Utils.helpFormat +import net.thauvin.erik.mobibot.Utils.isChannelOp +import org.pircbotx.hooks.types.GenericMessageEvent + +class Say : AbstractCommand() { + override val name = "say" + override val help = listOf("To have the bot say something on the channel:", helpFormat("%c $name ")) + override val isOpOnly = true + override val isPublic = false + override val isVisible = true + + override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) { + if (event.isChannelOp(channel)) { + event.bot().sendIRC().message(channel, args) + } + } +} diff --git a/bin/main/net/thauvin/erik/mobibot/commands/Users.kt b/bin/main/net/thauvin/erik/mobibot/commands/Users.kt new file mode 100644 index 0000000..33d6fef --- /dev/null +++ b/bin/main/net/thauvin/erik/mobibot/commands/Users.kt @@ -0,0 +1,50 @@ +/* + * Users.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package net.thauvin.erik.mobibot.commands + +import net.thauvin.erik.mobibot.Utils.bot +import net.thauvin.erik.mobibot.Utils.helpFormat +import net.thauvin.erik.mobibot.Utils.sendList +import org.pircbotx.hooks.types.GenericMessageEvent + +class Users : AbstractCommand() { + override val name = "users" + override val help = listOf("To list the users present on the channel:", helpFormat("%c $name")) + override val isOpOnly = false + override val isPublic = true + override val isVisible = true + + override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) { + val ch = event.bot().userChannelDao.getChannel(channel) + event.sendList(ch.users.map { if (it.channelsOpIn.contains(ch)) "@${it.nick}" else it.nick }, 8) + } +} diff --git a/bin/main/net/thauvin/erik/mobibot/commands/Versions.kt b/bin/main/net/thauvin/erik/mobibot/commands/Versions.kt new file mode 100644 index 0000000..896c569 --- /dev/null +++ b/bin/main/net/thauvin/erik/mobibot/commands/Versions.kt @@ -0,0 +1,59 @@ +/* + * Versions.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.thauvin.erik.mobibot.commands + +import net.thauvin.erik.mobibot.ReleaseInfo +import net.thauvin.erik.mobibot.Utils.helpFormat +import net.thauvin.erik.mobibot.Utils.isChannelOp +import net.thauvin.erik.mobibot.Utils.sendList +import net.thauvin.erik.mobibot.Utils.toIsoLocalDate +import org.pircbotx.PircBotX +import org.pircbotx.hooks.types.GenericMessageEvent + +class Versions : AbstractCommand() { + private val allVersions = listOf( + "Version: ${ReleaseInfo.VERSION} (${ReleaseInfo.BUILDDATE.toIsoLocalDate()})", + "${System.getProperty("os.name")} ${System.getProperty("os.version")} (${System.getProperty("os.arch")})" + + ", JVM ${System.getProperty("java.runtime.version")}", + "Kotlin ${KotlinVersion.CURRENT}, PircBotX ${PircBotX.VERSION}" + ) + override val name = "versions" + override val help = listOf("To view the versions data (bot, platform, java, etc.):", helpFormat("%c $name")) + override val isOpOnly = true + override val isPublic = false + override val isVisible = true + + override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) { + if (event.isChannelOp(channel)) { + event.sendList(allVersions, 1) + } + } +} diff --git a/bin/main/net/thauvin/erik/mobibot/commands/links/Comment.kt b/bin/main/net/thauvin/erik/mobibot/commands/links/Comment.kt new file mode 100644 index 0000000..1443d44 --- /dev/null +++ b/bin/main/net/thauvin/erik/mobibot/commands/links/Comment.kt @@ -0,0 +1,151 @@ +/* + * Comment.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package net.thauvin.erik.mobibot.commands.links + +import net.thauvin.erik.mobibot.Constants +import net.thauvin.erik.mobibot.Utils.bold +import net.thauvin.erik.mobibot.Utils.helpFormat +import net.thauvin.erik.mobibot.Utils.isChannelOp +import net.thauvin.erik.mobibot.Utils.sendMessage +import net.thauvin.erik.mobibot.commands.AbstractCommand +import net.thauvin.erik.mobibot.entries.EntriesUtils.printComment +import net.thauvin.erik.mobibot.entries.EntriesUtils.toLinkLabel +import net.thauvin.erik.mobibot.entries.EntryLink +import org.pircbotx.hooks.types.GenericMessageEvent + +class Comment : AbstractCommand() { + override val name = COMMAND + override val help = listOf( + "To add a comment:", + helpFormat("${Constants.LINK_CMD}1:This is a comment"), + "I will reply with a label, for example: ${Constants.LINK_CMD.bold()}1.1", + "To edit a comment, use its label: ", + helpFormat("${Constants.LINK_CMD}1.1:This is an edited comment"), + "To delete a comment, use its label and a minus sign: ", + helpFormat("${Constants.LINK_CMD}1.1:-") + ) + override val isOpOnly = false + override val isPublic = true + override val isVisible = true + + companion object { + const val COMMAND = "comment" + } + + override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) { + val cmds = args.substring(1).split("[.:]".toRegex(), 3) + val entryIndex = cmds[0].toInt() - 1 + + if (entryIndex < LinksManager.entries.links.size && LinksManager.isUpToDate(event)) { + val entry: EntryLink = LinksManager.entries.links[entryIndex] + val commentIndex = cmds[1].toInt() - 1 + if (commentIndex < entry.comments.size) { + when (val cmd = cmds[2].trim()) { + "" -> showComment(entry, entryIndex, commentIndex, event) // L1.1: + "-" -> deleteComment(channel, entry, entryIndex, commentIndex, event) // L1.1:- + else -> { + if (cmd.startsWith('?')) { // L1.1:? + changeAuthor(channel, cmd, entry, entryIndex, commentIndex, event) + } else { // L1.1: + setComment(cmd, entry, entryIndex, commentIndex, event) + } + } + } + } + } + } + + override fun helpResponse(channel: String, topic: String, event: GenericMessageEvent): Boolean { + if (super.helpResponse(channel, topic, event)) { + if (event.isChannelOp(channel)) { + event.sendMessage("To change a comment's author:") + event.sendMessage(helpFormat("${Constants.LINK_CMD}1.1:?")) + } + return true + } + return false + } + + override fun matches(message: String): Boolean { + return message.matches("^${Constants.LINK_CMD}\\d+\\.\\d+:.*".toRegex()) + } + + private fun changeAuthor( + channel: String, + cmd: String, + entry: EntryLink, + entryIndex: Int, + commentIndex: Int, + event: GenericMessageEvent + ) { + if (event.isChannelOp(channel) && cmd.length > 1) { + val comment = entry.getComment(commentIndex) + comment.nick = cmd.substring(1) + event.sendMessage(printComment(entryIndex, commentIndex, comment)) + LinksManager.entries.save() + } else { + event.sendMessage("Please ask a channel op to change the author of this comment for you.") + } + } + + private fun deleteComment( + channel: String, + entry: EntryLink, + entryIndex: Int, + commentIndex: Int, + event: GenericMessageEvent + ) { + if (event.isChannelOp(channel) || event.user.nick == entry.getComment(commentIndex).nick) { + entry.deleteComment(commentIndex) + event.sendMessage("Comment ${entryIndex.toLinkLabel()}.${commentIndex + 1} removed.") + LinksManager.entries.save() + } else { + event.sendMessage("Please ask a channel op to delete this comment for you.") + } + } + + private fun setComment( + cmd: String, + entry: EntryLink, + entryIndex: Int, + commentIndex: Int, + event: GenericMessageEvent + ) { + entry.setComment(commentIndex, cmd, event.user.nick) + event.sendMessage(printComment(entryIndex, commentIndex, entry.getComment(commentIndex))) + LinksManager.entries.save() + } + + private fun showComment(entry: EntryLink, entryIndex: Int, commentIndex: Int, event: GenericMessageEvent) { + event.sendMessage(printComment(entryIndex, commentIndex, entry.getComment(commentIndex))) + } +} diff --git a/bin/main/net/thauvin/erik/mobibot/commands/links/LinksManager.kt b/bin/main/net/thauvin/erik/mobibot/commands/links/LinksManager.kt new file mode 100644 index 0000000..fba6b99 --- /dev/null +++ b/bin/main/net/thauvin/erik/mobibot/commands/links/LinksManager.kt @@ -0,0 +1,207 @@ +/* + * LinksManager.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package net.thauvin.erik.mobibot.commands.links + +import net.thauvin.erik.mobibot.Constants +import net.thauvin.erik.mobibot.Pinboard +import net.thauvin.erik.mobibot.Utils.bold +import net.thauvin.erik.mobibot.Utils.bot +import net.thauvin.erik.mobibot.Utils.helpFormat +import net.thauvin.erik.mobibot.Utils.sendMessage +import net.thauvin.erik.mobibot.Utils.today +import net.thauvin.erik.mobibot.commands.AbstractCommand +import net.thauvin.erik.mobibot.commands.Ignore.Companion.isNotIgnored +import net.thauvin.erik.mobibot.entries.Entries +import net.thauvin.erik.mobibot.entries.EntriesUtils.printLink +import net.thauvin.erik.mobibot.entries.EntriesUtils.toLinkLabel +import net.thauvin.erik.mobibot.entries.EntryLink +import net.thauvin.erik.mobibot.social.SocialManager +import org.jsoup.Jsoup +import org.pircbotx.hooks.types.GenericMessageEvent +import java.io.IOException + +class LinksManager : AbstractCommand() { + private val defaultTags: MutableList = mutableListOf() + private val keywords: MutableList = mutableListOf() + + override val name = Constants.LINK_CMD + override val help = emptyList() + override val isOpOnly = false + override val isPublic = false + override val isVisible = false + + init { + initProperties(TAGS_PROP, KEYWORDS_PROP) + } + + companion object { + val LINK_MATCH = "^[hH][tT][tT][pP](|[sS])://.*".toRegex() + const val KEYWORDS_PROP = "tags-keywords" + const val TAGS_PROP = "tags" + val TAG_MATCH = ", *| +".toRegex() + + /** + * Entries array + */ + @JvmField + val entries = Entries() + + /** + * Pinboard handler. + */ + @JvmField + val pinboard = Pinboard() + + /** + * Social Manager handler. + */ + @JvmField + val socialManager = SocialManager() + + /** + * Let the user know if the entries are too old to be modified. + */ + @JvmStatic + fun isUpToDate(event: GenericMessageEvent): Boolean { + if (entries.lastPubDate != today()) { + event.sendMessage("The links are too old to be updated.") + return false + } + return true + } + } + + override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) { + val cmds = args.split(" ".toRegex(), 2) + val sender = event.user.nick + val botNick = event.bot().nick + val login = event.user.login + + if (isNotIgnored(sender) && (cmds.size == 1 || !cmds[1].contains(botNick))) { + val link = cmds[0].trim() + if (!isDupEntry(link, event)) { + var title = "" + val tags = ArrayList(defaultTags) + if (cmds.size == 2) { + val data = cmds[1].trim().split("${Tags.COMMAND}:", limit = 2) + title = data[0].trim() + if (data.size > 1) { + tags.addAll(data[1].split(TAG_MATCH)) + } + } + + if (title.isBlank()) { + title = fetchTitle(link) + } + + if (title != Constants.NO_TITLE) { + matchTagKeywords(title, tags) + } + + // Links are old, clear them + if (entries.lastPubDate != today()) { + entries.links.clear() + } + + val entry = EntryLink(link, title, sender, login, channel, tags) + entries.links.add(entry) + val index = entries.links.lastIndexOf(entry) + event.sendMessage(printLink(index, entry)) + + pinboard.addPin(event.bot().serverHostname, entry) + + // Queue link for posting to social media. + socialManager.queueEntry(index) + + entries.save() + + if (Constants.NO_TITLE == entry.title) { + event.sendMessage("Please specify a title, by typing:") + event.sendMessage(helpFormat("${index.toLinkLabel()}:|This is the title")) + } + } + } + } + + override fun helpResponse(channel: String, topic: String, event: GenericMessageEvent): Boolean = false + + override fun matches(message: String): Boolean { + return message.matches(LINK_MATCH) + } + + internal fun fetchTitle(link: String): String { + try { + val html = Jsoup.connect(link) + .userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:74.0) Gecko/20100101 Firefox/74.0") + .get() + val title = html.title() + if (title.isNotBlank()) { + return title + } + } catch (ignore: IOException) { + // Do nothing + } + return Constants.NO_TITLE + } + + private fun isDupEntry(link: String, event: GenericMessageEvent): Boolean { + synchronized(entries) { + return try { + val match = entries.links.single { it.link == link } + event.sendMessage( + "Duplicate".bold() + " >> " + printLink(entries.links.indexOf(match), match) + ) + true + } catch (ignore: NoSuchElementException) { + false + } + } + } + + internal fun matchTagKeywords(title: String, tags: MutableList) { + for (match in keywords) { + val m = Regex.escape(match) + if (title.matches("(?i).*\\b$m\\b.*".toRegex())) { + tags.add(match) + } + } + } + + override fun setProperty(key: String, value: String) { + super.setProperty(key, value) + if (KEYWORDS_PROP == key) { + keywords.addAll(value.split(TAG_MATCH)) + } else if (TAGS_PROP == key) { + defaultTags.addAll(value.split(TAG_MATCH)) + } + } +} diff --git a/bin/main/net/thauvin/erik/mobibot/commands/links/Posting.kt b/bin/main/net/thauvin/erik/mobibot/commands/links/Posting.kt new file mode 100644 index 0000000..ff4278d --- /dev/null +++ b/bin/main/net/thauvin/erik/mobibot/commands/links/Posting.kt @@ -0,0 +1,164 @@ +/* + * Posting.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package net.thauvin.erik.mobibot.commands.links + +import net.thauvin.erik.mobibot.Constants +import net.thauvin.erik.mobibot.Utils.bold +import net.thauvin.erik.mobibot.Utils.bot +import net.thauvin.erik.mobibot.Utils.helpFormat +import net.thauvin.erik.mobibot.Utils.isChannelOp +import net.thauvin.erik.mobibot.Utils.sendMessage +import net.thauvin.erik.mobibot.commands.AbstractCommand +import net.thauvin.erik.mobibot.commands.links.LinksManager.Companion.entries +import net.thauvin.erik.mobibot.entries.EntriesUtils +import net.thauvin.erik.mobibot.entries.EntriesUtils.toLinkLabel +import net.thauvin.erik.mobibot.entries.EntryLink +import org.pircbotx.hooks.types.GenericMessageEvent + +class Posting : AbstractCommand() { + override val name = "posting" + override val help = listOf( + "Post a URL, by saying it on a line on its own:", + helpFormat(" [] ${Tags.COMMAND}: <+tag> [...]]"), + "I will reply with a label, for example: ${Constants.LINK_CMD.bold()}1", + "To add a title, use its label and a pipe:", + helpFormat("${Constants.LINK_CMD}1:|This is the title"), + "To add a comment:", + helpFormat("${Constants.LINK_CMD}1:This is a comment"), + "I will reply with a label, for example: ${Constants.LINK_CMD.bold()}1.1", + "To edit a comment, see: ", + helpFormat("%c ${Constants.HELP_CMD} ${Comment.COMMAND}") + ) + override val isOpOnly = false + override val isPublic = true + override val isVisible = true + + override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) { + val cmds = args.substring(1).split(":", limit = 2) + val entryIndex = cmds[0].toInt() - 1 + + if (entryIndex < entries.links.size) { + val cmd = cmds[1].trim() + if (cmd.isBlank()) { + showEntry(entryIndex, event) // L1: + } else if (LinksManager.isUpToDate(event)) { + if (cmd == "-") { + removeEntry(channel, entryIndex, event) // L1:- + } else { + when (cmd[0]) { + '|' -> changeTitle(cmd, entryIndex, event) // L1:|<title> + '=' -> changeUrl(channel, cmd, entryIndex, event) // L1:=<url> + '?' -> changeAuthor(channel, cmd, entryIndex, event) // L1:?<author> + else -> addComment(cmd, entryIndex, event) // L1:<comment> + } + } + } + } + } + + override fun matches(message: String): Boolean { + return message.matches("${Constants.LINK_CMD}\\d+:.*".toRegex()) + } + + private fun addComment(cmd: String, entryIndex: Int, event: GenericMessageEvent) { + val entry: EntryLink = entries.links[entryIndex] + val commentIndex = entry.addComment(cmd, event.user.nick) + val comment = entry.getComment(commentIndex) + event.sendMessage(EntriesUtils.printComment(entryIndex, commentIndex, comment)) + entries.save() + } + + private fun changeTitle(cmd: String, entryIndex: Int, event: GenericMessageEvent) { + if (cmd.length > 1) { + val entry: EntryLink = entries.links[entryIndex] + entry.title = cmd.substring(1).trim() + LinksManager.pinboard.updatePin(event.bot().serverHostname, entry.link, entry) + event.sendMessage(EntriesUtils.printLink(entryIndex, entry)) + entries.save() + } + } + + private fun changeUrl(channel: String, cmd: String, entryIndex: Int, event: GenericMessageEvent) { + val entry: EntryLink = entries.links[entryIndex] + if (entry.login == event.user.login || event.isChannelOp(channel)) { + val link = cmd.substring(1) + if (link.matches(LinksManager.LINK_MATCH)) { + val oldLink = entry.link + entry.link = link + LinksManager.pinboard.updatePin(event.bot().serverHostname, oldLink, entry) + event.sendMessage(EntriesUtils.printLink(entryIndex, entry)) + entries.save() + } + } + } + + private fun changeAuthor(channel: String, cmd: String, index: Int, event: GenericMessageEvent) { + if (event.isChannelOp(channel)) { + if (cmd.length > 1) { + val entry: EntryLink = entries.links[index] + entry.nick = cmd.substring(1) + LinksManager.pinboard.updatePin(event.bot().serverHostname, entry.link, entry) + event.sendMessage(EntriesUtils.printLink(index, entry)) + entries.save() + } + } else { + event.sendMessage("Please ask a channel op to change the author of this link for you.") + } + } + + private fun removeEntry(channel: String, index: Int, event: GenericMessageEvent) { + val entry: EntryLink = entries.links[index] + if (entry.login == event.user.login || event.isChannelOp(channel)) { + LinksManager.pinboard.deletePin(entry) + LinksManager.socialManager.removeEntry(index) + entries.links.removeAt(index) + event.sendMessage("Entry ${index.toLinkLabel()} removed.") + entries.save() + } else { + event.sendMessage("Please ask a channel op to remove this entry for you.") + } + } + + private fun showEntry(index: Int, event: GenericMessageEvent) { + val entry: EntryLink = entries.links[index] + event.sendMessage(EntriesUtils.printLink(index, entry)) + if (entry.tags.isNotEmpty()) { + event.sendMessage(EntriesUtils.printTags(index, entry)) + } + if (entry.comments.isNotEmpty()) { + val comments = entry.comments + for (i in comments.indices) { + event.sendMessage(EntriesUtils.printComment(index, i, comments[i])) + } + } + } +} diff --git a/bin/main/net/thauvin/erik/mobibot/commands/links/Tags.kt b/bin/main/net/thauvin/erik/mobibot/commands/links/Tags.kt new file mode 100644 index 0000000..1662857 --- /dev/null +++ b/bin/main/net/thauvin/erik/mobibot/commands/links/Tags.kt @@ -0,0 +1,87 @@ +/* + * Tags.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package net.thauvin.erik.mobibot.commands.links + +import net.thauvin.erik.mobibot.Constants +import net.thauvin.erik.mobibot.Utils.bot +import net.thauvin.erik.mobibot.Utils.helpFormat +import net.thauvin.erik.mobibot.Utils.isChannelOp +import net.thauvin.erik.mobibot.Utils.sendMessage +import net.thauvin.erik.mobibot.commands.AbstractCommand +import net.thauvin.erik.mobibot.entries.EntriesUtils +import net.thauvin.erik.mobibot.entries.EntryLink +import org.pircbotx.hooks.types.GenericMessageEvent + +class Tags : AbstractCommand() { + override val name = COMMAND + override val help = listOf( + "To categorize or tag a URL, use its label and a ${Constants.TAG_CMD}:", + helpFormat("${Constants.LINK_CMD}1${Constants.TAG_CMD}:<+tag|-tag> [...]") + ) + override val isOpOnly = false + override val isPublic = true + override val isVisible = true + + companion object { + const val COMMAND = "tags" + } + + override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) { + val cmds = args.substring(1).split("${Constants.TAG_CMD}:", limit = 2) + val index = cmds[0].toInt() - 1 + + if (index < LinksManager.entries.links.size && LinksManager.isUpToDate(event)) { + val cmd = cmds[1].trim() + val entry: EntryLink = LinksManager.entries.links[index] + if (cmd.isNotEmpty()) { + if (entry.login == event.user.login || event.isChannelOp(channel)) { + entry.setTags(cmd) + LinksManager.pinboard.updatePin(event.bot().serverHostname, entry.link, entry) + event.sendMessage(EntriesUtils.printTags(index, entry)) + LinksManager.entries.save() + } else { + event.sendMessage("Please ask a channel op to change the tags for you.") + } + } else { + if (entry.tags.isNotEmpty()) { + event.sendMessage(EntriesUtils.printTags(index, entry)) + } else { + event.sendMessage("The entry has no tags. Why don't add some?") + } + } + } + } + + override fun matches(message: String): Boolean { + return message.matches("^${Constants.LINK_CMD}\\d+${Constants.TAG_CMD}:.*".toRegex()) + } +} diff --git a/bin/main/net/thauvin/erik/mobibot/commands/links/View.kt b/bin/main/net/thauvin/erik/mobibot/commands/links/View.kt new file mode 100644 index 0000000..825e374 --- /dev/null +++ b/bin/main/net/thauvin/erik/mobibot/commands/links/View.kt @@ -0,0 +1,120 @@ +/* + * View.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package net.thauvin.erik.mobibot.commands.links + +import net.thauvin.erik.mobibot.Utils.bot +import net.thauvin.erik.mobibot.Utils.helpCmdSyntax +import net.thauvin.erik.mobibot.Utils.helpFormat +import net.thauvin.erik.mobibot.Utils.lastOrEmpty +import net.thauvin.erik.mobibot.Utils.sendMessage +import net.thauvin.erik.mobibot.commands.AbstractCommand +import net.thauvin.erik.mobibot.commands.links.LinksManager.Companion.entries +import net.thauvin.erik.mobibot.entries.EntriesUtils +import net.thauvin.erik.mobibot.entries.EntryLink +import org.pircbotx.hooks.events.PrivateMessageEvent +import org.pircbotx.hooks.types.GenericMessageEvent + +class View : AbstractCommand() { + override val name = VIEW_CMD + override val help = listOf( + "To list or search the current URL posts:", + helpFormat("%c $name [<start>] [<query>]") + ) + 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<Int, String> { + 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 [<start>] [<query>] + val split = query.split(" ", limit = 2) + try { + start = split[0].toInt() - 1 + query = split.lastOrEmpty().trim() + if (start > entries.links.size) { + start = 0 + } + } catch (ignore: NumberFormatException) { + // Do nothing + } + } + return Pair(start, query) + } + + private fun showPosts(start: Int, query: String, event: GenericMessageEvent) { + var index = start + var entry: EntryLink + var sent = 0 + while (index < entries.links.size && sent < MAX_ENTRIES) { + entry = entries.links[index] + if (query.isNotBlank()) { + if (entry.matches(query)) { + event.sendMessage(EntriesUtils.printLink(index, entry, true)) + sent++ + } + } else { + event.sendMessage(EntriesUtils.printLink(index, entry, true)) + sent++ + } + index++ + if (sent == MAX_ENTRIES && index < entries.links.size) { + event.sendMessage("To view more, try: ") + event.sendMessage( + helpFormat( + helpCmdSyntax("%c $name ${index + 1} $query", event.bot().nick, event is PrivateMessageEvent) + ) + ) + } + } + if (sent == 0) { + event.sendMessage("No matches. Please try again.") + } + } +} diff --git a/bin/main/net/thauvin/erik/mobibot/commands/seen/NickComparator.kt b/bin/main/net/thauvin/erik/mobibot/commands/seen/NickComparator.kt new file mode 100644 index 0000000..cfd2c27 --- /dev/null +++ b/bin/main/net/thauvin/erik/mobibot/commands/seen/NickComparator.kt @@ -0,0 +1,45 @@ +/* + * NickComparator.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package net.thauvin.erik.mobibot.commands.seen + +import java.io.Serializable + +class NickComparator : Comparator<String>, Serializable { + override fun compare(a: String, b: String): Int { + return a.lowercase().compareTo(b.lowercase()) + } + + companion object { + @Suppress("ConstPropertyName") + private const val serialVersionUID = 1L + } +} diff --git a/bin/main/net/thauvin/erik/mobibot/commands/seen/Seen.kt b/bin/main/net/thauvin/erik/mobibot/commands/seen/Seen.kt new file mode 100644 index 0000000..c9ee0f3 --- /dev/null +++ b/bin/main/net/thauvin/erik/mobibot/commands/seen/Seen.kt @@ -0,0 +1,150 @@ +/* + * Seen.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package net.thauvin.erik.mobibot.commands.seen + +import com.google.common.collect.ImmutableSortedSet +import net.thauvin.erik.mobibot.Utils +import net.thauvin.erik.mobibot.Utils.bold +import net.thauvin.erik.mobibot.Utils.bot +import net.thauvin.erik.mobibot.Utils.helpFormat +import net.thauvin.erik.mobibot.Utils.isChannelOp +import net.thauvin.erik.mobibot.Utils.loadSerialData +import net.thauvin.erik.mobibot.Utils.saveSerialData +import net.thauvin.erik.mobibot.Utils.sendList +import net.thauvin.erik.mobibot.Utils.sendMessage +import net.thauvin.erik.mobibot.commands.AbstractCommand +import net.thauvin.erik.mobibot.commands.Info.Companion.toUptime +import org.pircbotx.User +import org.pircbotx.hooks.types.GenericMessageEvent +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.util.* + + +class Seen(private val serialObject: String) : AbstractCommand() { + private val logger: Logger = LoggerFactory.getLogger(Seen::class.java) + private val allKeyword = "all" + val seenNicks = TreeMap<String, SeenNick>(NickComparator()) + + override val name = "seen" + override val help = listOf("To view when a nickname was last seen:", helpFormat("%c $name <nick>")) + private val helpOp = help.plus( + arrayOf("To view all ${"seen".bold()} nicks:", helpFormat("%c $name $allKeyword")) + ) + override val isOpOnly = false + override val isPublic = true + override val isVisible = true + + + override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) { + if (isEnabled()) { + if (args.isNotBlank() && !args.contains(' ')) { + val ch = event.bot().userChannelDao.getChannel(channel) + if (args == allKeyword && ch.isOp(event.user) && seenNicks.isNotEmpty()) { + event.sendMessage("The ${"seen".bold()} nicks are:") + event.sendList(seenNicks.keys.toList(), 7, separator = ", ", isIndent = true) + return + } + ch.users.forEach { + if (args.equals(it.nick, true)) { + event.sendMessage("${it.nick} is on ${channel}.") + return + } + } + if (seenNicks.containsKey(args)) { + val seenNick = seenNicks.getValue(args) + val lastSeen = System.currentTimeMillis() - seenNick.lastSeen + event.sendMessage("${seenNick.nick} was last seen on $channel ${lastSeen.toUptime()} ago.") + return + } + event.sendMessage("I haven't seen $args on $channel lately.") + } else { + helpResponse(channel, args, event) + } + } + } + + fun add(nick: String) { + if (isEnabled()) { + seenNicks[nick] = SeenNick(nick, System.currentTimeMillis()) + save() + } + } + + fun add(users: ImmutableSortedSet<User>) { + if (isEnabled()) { + users.forEach { + seenNicks[it.nick] = SeenNick(it.nick, System.currentTimeMillis()) + } + save() + } + } + + fun clear() { + seenNicks.clear() + } + + fun count(): Int = seenNicks.size + + override fun helpResponse(channel: String, topic: String, event: GenericMessageEvent): Boolean { + return if (event.isChannelOp(channel)) { + for (h in helpOp) { + event.sendMessage(Utils.helpCmdSyntax(h, event.bot().nick, true)) + } + true + } else { + super.helpResponse(channel, topic, event) + } + } + + fun load() { + if (isEnabled()) { + @Suppress("UNCHECKED_CAST") + seenNicks.putAll( + loadSerialData( + serialObject, + TreeMap<String, SeenNick>(), + logger, + "seen nicknames" + ) as TreeMap<String, SeenNick> + ) + } + } + + fun save() { + saveSerialData(serialObject, seenNicks, logger, "seen nicknames") + } + + init { + load() + } +} diff --git a/bin/main/net/thauvin/erik/mobibot/commands/seen/SeenNick.kt b/bin/main/net/thauvin/erik/mobibot/commands/seen/SeenNick.kt new file mode 100644 index 0000000..7924977 --- /dev/null +++ b/bin/main/net/thauvin/erik/mobibot/commands/seen/SeenNick.kt @@ -0,0 +1,41 @@ +/* + * SeenNick.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package net.thauvin.erik.mobibot.commands.seen + +import java.io.Serializable + +data class SeenNick(val nick: String, val lastSeen: Long) : Serializable { + companion object { + @Suppress("ConstPropertyName") + private const val serialVersionUID = 1L + } +} diff --git a/bin/main/net/thauvin/erik/mobibot/commands/tell/Tell.kt b/bin/main/net/thauvin/erik/mobibot/commands/tell/Tell.kt new file mode 100644 index 0000000..061ca6a --- /dev/null +++ b/bin/main/net/thauvin/erik/mobibot/commands/tell/Tell.kt @@ -0,0 +1,306 @@ +/* + * Tell.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.thauvin.erik.mobibot.commands.tell + +import net.thauvin.erik.mobibot.Utils.bold +import net.thauvin.erik.mobibot.Utils.bot +import net.thauvin.erik.mobibot.Utils.helpCmdSyntax +import net.thauvin.erik.mobibot.Utils.helpFormat +import net.thauvin.erik.mobibot.Utils.isChannelOp +import net.thauvin.erik.mobibot.Utils.plural +import net.thauvin.erik.mobibot.Utils.reverseColor +import net.thauvin.erik.mobibot.Utils.sendMessage +import net.thauvin.erik.mobibot.Utils.toIntOrDefault +import net.thauvin.erik.mobibot.Utils.toUtcDateTime +import net.thauvin.erik.mobibot.commands.AbstractCommand +import net.thauvin.erik.mobibot.commands.links.View +import org.pircbotx.PircBotX +import org.pircbotx.hooks.events.MessageEvent +import org.pircbotx.hooks.types.GenericMessageEvent +import org.pircbotx.hooks.types.GenericUserEvent + +/** + * The `Tell` command. + */ +class Tell(private val serialObject: String) : AbstractCommand() { + // Messages queue + private val messages: MutableList<TellMessage> = 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 <nick> <message>"), + "To view queued and sent messages:", + helpFormat("%c $name ${View.VIEW_CMD}"), + "Messages are kept for ${maxDays.bold()}" + " day".plural(maxDays.toLong()) + '.' + ) + override val isOpOnly: Boolean = false + override val isPublic: Boolean = isEnabled() + override val isVisible: Boolean = isEnabled() + + /** + * Cleans the messages queue. + */ + private fun clean(): Boolean { + return TellManager.clean(messages, maxDays.toLong()) + } + + override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) { + if (isEnabled()) { + when { + args.isBlank() -> { + helpResponse(channel, args, event) + } + + args.startsWith(View.VIEW_CMD) -> { + if (event.isChannelOp(channel) && "${View.VIEW_CMD} $TELL_ALL_KEYWORD" == args) { + viewAll(event) + } else { + viewMessages(event) + } + } + + args.startsWith("$TELL_DEL_KEYWORD ") -> { + deleteMessage(channel, args, event) + } + + else -> { + newMessage(channel, args, event) + } + } + if (clean()) { + save() + } + } + } + + // Delete message. + private fun deleteMessage(channel: String, args: String, event: GenericMessageEvent) { + val split = args.split(" ") + if (split.size == 2) { + val id = split[1] + if (TELL_ALL_KEYWORD.equals(id, ignoreCase = true)) { + if (messages.removeIf { it.sender.equals(event.user.nick, true) && it.isReceived }) { + save() + event.sendMessage("Delivered messages have been deleted.") + } else { + event.sendMessage("No delivered messages were found.") + } + } else { + if (messages.removeIf { + it.id == id && + (it.sender.equals(event.user.nick, true) || event.isChannelOp(channel)) + }) { + save() + event.sendMessage("The message was deleted from the queue.") + } else { + event.sendMessage("The specified message [ID $id] could not be found.") + } + } + } else { + helpResponse(channel, args, event) + } + } + + override fun isEnabled(): Boolean { + return maxSize > 0 && maxDays > 0 + } + + override fun setProperty(key: String, value: String) { + super.setProperty(key, value) + if (MAX_DAYS_PROP == key) { + maxDays = value.toIntOrDefault(maxDays) + } else if (MAX_SIZE_PROP == key) { + maxSize = value.toIntOrDefault(maxSize) + } + } + + // New message. + private fun newMessage(channel: String, args: String, event: GenericMessageEvent) { + val split = args.split(" ".toRegex(), 2) + if (split.size == 2 && split[1].isNotBlank() && split[1].contains(" ")) { + if (messages.size < maxSize) { + val message = TellMessage(event.user.nick, split[0], split[1].trim()) + messages.add(message) + save() + event.sendMessage("Message [ID ${message.id}] was queued for ${message.recipient.bold()}") + } else { + event.sendMessage("Sorry, the messages queue is currently full.") + } + } else { + helpResponse(channel, args, event) + } + } + + /** + * Saves the messages queue. + */ + private fun save() { + TellManager.save(serialObject, messages) + } + + /** + * Checks and sends messages. + */ + fun send(event: GenericUserEvent) { + val nickname = event.user.nick + if (isEnabled() && nickname != event.getBot<PircBotX>().nick) { + messages.filter { it.isMatch(nickname) }.forEach { message -> + if (message.recipient.equals(nickname, ignoreCase = true) && !message.isReceived) { + if (message.sender == nickname) { + if (event !is MessageEvent) { + event.user.send().message( + "${"You".bold()} wanted me to remind you: ${message.message.reverseColor()}" + ) + message.isReceived = true + message.isNotified = true + save() + } + } else { + event.user.send().message( + "${message.sender} wanted me to tell you: ${message.message.reverseColor()}" + ) + message.isReceived = true + save() + } + } else if (message.sender.equals(nickname, ignoreCase = true) && message.isReceived + && !message.isNotified + ) { + event.user.send().message( + "Your message ${"[ID ${message.id}]".reverseColor()} was sent to " + + "${message.recipient.bold()} on ${message.receptionDate}" + ) + message.isNotified = true + save() + } + } + } + } + + /** + * Returns the messages queue size. + * + * @return The size. + */ + fun size(): Int = messages.size + + // View all messages. + private fun viewAll(event: GenericMessageEvent) { + if (messages.isNotEmpty()) { + for (message in messages) { + event.sendMessage( + "${message.sender.bold()}$ARROW${message.recipient.bold()} [ID: ${message.id}, " + + (if (message.isReceived) "DELIVERED]" else "QUEUED]") + ) + } + } else { + event.sendMessage("There are no messages in the queue.") + } + } + + // View messages. + private fun viewMessages(event: GenericMessageEvent) { + var hasMessage = false + for (message in messages.filter { it.isMatch(event.user.nick) }) { + if (!hasMessage) { + hasMessage = true + event.sendMessage("Here are your messages: ") + } + if (message.isReceived) { + event.sendMessage( + message.sender.bold() + ARROW + message.recipient.bold() + + " [${message.receptionDate.toUtcDateTime()}, ID: ${message.id.bold()}, DELIVERED]" + ) + } else { + event.sendMessage( + message.sender.bold() + ARROW + message.recipient.bold() + + " [${message.queued.toUtcDateTime()}, ID: ${message.id.bold()}, QUEUED]" + ) + } + event.sendMessage(helpFormat(message.message)) + } + if (!hasMessage) { + event.sendMessage("You have no messages in the queue.") + } else { + event.sendMessage("To delete one or all delivered messages:") + event.sendMessage( + helpFormat( + helpCmdSyntax("%c $name $TELL_DEL_KEYWORD <id|$TELL_ALL_KEYWORD>", event.bot().nick, true) + ) + ) + event.sendMessage(help.last()) + } + } + + companion object { + /** + * Max days property. + */ + const val MAX_DAYS_PROP = "tell-max-days" + + /** + * Max size property. + */ + const val MAX_SIZE_PROP = "tell-max-size" + + // Arrow + private const val ARROW = " --> " + + // All keyword + private const val TELL_ALL_KEYWORD = "all" + + //T he delete command. + private const val TELL_DEL_KEYWORD = "del" + } + + /** + * Creates a new instance. + */ + init { + initProperties(MAX_DAYS_PROP, MAX_SIZE_PROP) + + // Load the message queue + messages.addAll(TellManager.load(serialObject)) + if (clean()) { + save() + } + } +} diff --git a/bin/main/net/thauvin/erik/mobibot/commands/tell/TellManager.kt b/bin/main/net/thauvin/erik/mobibot/commands/tell/TellManager.kt new file mode 100644 index 0000000..b65a4da --- /dev/null +++ b/bin/main/net/thauvin/erik/mobibot/commands/tell/TellManager.kt @@ -0,0 +1,74 @@ +/* + * TellManager.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.thauvin.erik.mobibot.commands.tell + +import net.thauvin.erik.mobibot.Utils.loadSerialData +import net.thauvin.erik.mobibot.Utils.saveSerialData +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.time.Clock +import java.time.LocalDateTime + +/** + * The Tell Messages Manager. + */ +object TellManager { + private val logger: Logger = LoggerFactory.getLogger(TellManager::class.java) + + /** + * Cleans the messages queue. + */ + @JvmStatic + fun clean(tellMessages: MutableList<TellMessage>, 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<TellMessage> { + @Suppress("UNCHECKED_CAST") + return loadSerialData(file, emptyList<TellMessage>(), logger, "message queue") as List<TellMessage> + } + + /** + * Saves the messages. + */ + @JvmStatic + fun save(file: String, messages: List<TellMessage?>?) { + if (messages != null) { + saveSerialData(file, messages, logger, "messages") + } + } +} diff --git a/bin/main/net/thauvin/erik/mobibot/commands/tell/TellMessage.kt b/bin/main/net/thauvin/erik/mobibot/commands/tell/TellMessage.kt new file mode 100644 index 0000000..d17fbb5 --- /dev/null +++ b/bin/main/net/thauvin/erik/mobibot/commands/tell/TellMessage.kt @@ -0,0 +1,104 @@ +/* + * TellMessage.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.thauvin.erik.mobibot.commands.tell + +import java.io.Serializable +import java.time.Clock +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter + +/** + * Tell Message. + */ +class TellMessage( + /** + * Returns the message's sender. + */ + val sender: String, + + /** + * Returns the message's recipient. + */ + val recipient: String, + + /** + * Returns the message text. + */ + val message: String +) : Serializable { + /** + * Returns the queued date/time. + */ + var queued: LocalDateTime = LocalDateTime.now(Clock.systemUTC()) + + /** + * Returns the message id. + */ + var id: String = queued.format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")) + + /** + * Returns `true` if a notification was sent. + */ + var isNotified = false + + /** + * Returns `true` if the message was received. + */ + var isReceived = false + set(value) { + if (value) { + receptionDate = LocalDateTime.now(Clock.systemUTC()) + } + field = value + } + + /** + * Returns the message creating date. + */ + var receptionDate: LocalDateTime = LocalDateTime.MIN + + /** + * Matches the message sender or recipient. + */ + fun isMatch(nick: String?): Boolean { + return sender.equals(nick, ignoreCase = true) || recipient.equals(nick, ignoreCase = true) + } + + override fun toString(): String { + return ("TellMessage{id='$id', isNotified=$isNotified, isReceived=$isReceived, message='$message', " + + "queued=$queued, received=$receptionDate, recipient='$recipient', sender='$sender'}") + } + + companion object { + @Suppress("ConstPropertyName") + private const val serialVersionUID = 2L + } +} diff --git a/bin/main/net/thauvin/erik/mobibot/entries/Entries.kt b/bin/main/net/thauvin/erik/mobibot/entries/Entries.kt new file mode 100644 index 0000000..e8676ec --- /dev/null +++ b/bin/main/net/thauvin/erik/mobibot/entries/Entries.kt @@ -0,0 +1,54 @@ +/* + * Entries.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package net.thauvin.erik.mobibot.entries + +import net.thauvin.erik.mobibot.Utils.today + +class Entries( + var channel: String = "", + var ircServer: String = "", + var logsDir: String = "", + var backlogs: String = "" +) { + val links = mutableListOf<EntryLink>() + + var lastPubDate = today() + + fun load() { + lastPubDate = FeedsManager.loadFeed(this) + } + + fun save() { + lastPubDate = today() + FeedsManager.saveFeed(this) + } +} diff --git a/bin/main/net/thauvin/erik/mobibot/entries/EntriesUtils.kt b/bin/main/net/thauvin/erik/mobibot/entries/EntriesUtils.kt new file mode 100644 index 0000000..9c09626 --- /dev/null +++ b/bin/main/net/thauvin/erik/mobibot/entries/EntriesUtils.kt @@ -0,0 +1,83 @@ +/* + * EntriesUtils.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.thauvin.erik.mobibot.entries + +import net.thauvin.erik.mobibot.Constants +import net.thauvin.erik.mobibot.Utils.bold +import net.thauvin.erik.mobibot.Utils.green + +/** + * Entries utilities. + */ +object EntriesUtils { + /** + * Prints an entry's comment for display on the channel. + */ + @JvmStatic + fun printComment(entryIndex: Int, commentIndex: Int, comment: EntryComment): String = + ("${entryIndex.toLinkLabel()}.${commentIndex + 1}: [${comment.nick}] ${comment.comment}") + + /** + * Prints an entry's link for display on the channel. + */ + @JvmStatic + @JvmOverloads + fun printLink(entryIndex: Int, entry: EntryLink, isView: Boolean = false): String { + val buff = StringBuilder().append(entryIndex.toLinkLabel()).append(": ") + .append('[').append(entry.nick).append(']') + if (isView && entry.comments.isNotEmpty()) { + buff.append("[+").append(entry.comments.size).append(']') + } + buff.append(' ') + with(entry) { + if (Constants.NO_TITLE == title) { + buff.append(title) + } else { + buff.append(title.bold()) + } + buff.append(" ( ").append(link.green()).append(" )") + } + return buff.toString() + } + + /** + * Prints an entry's tags/categories for display on the channel. e.g. L1T: tag1, tag2 + */ + @JvmStatic + fun printTags(entryIndex: Int, entry: EntryLink): String = + entryIndex.toLinkLabel() + "${Constants.TAG_CMD}: " + entry.formatTags(", ") + + /** + * Builds link label based on its index. e.g: L1 + */ + @JvmStatic + fun Int.toLinkLabel(): String = Constants.LINK_CMD + (this + 1) +} diff --git a/bin/main/net/thauvin/erik/mobibot/entries/EntryComment.kt b/bin/main/net/thauvin/erik/mobibot/entries/EntryComment.kt new file mode 100644 index 0000000..e18d692 --- /dev/null +++ b/bin/main/net/thauvin/erik/mobibot/entries/EntryComment.kt @@ -0,0 +1,52 @@ +/* + * EntryComment.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.thauvin.erik.mobibot.entries + +import java.io.Serializable +import java.time.LocalDateTime + +/** + * Entry comments data class. + */ +data class EntryComment(var comment: String, var nick: String) : Serializable { + /** + * Creation date. + */ + val date: LocalDateTime = LocalDateTime.now() + + override fun toString(): String = "EntryComment{comment='$comment', date=$date, nick='$nick'}" + + companion object { + // Serial version UID + @Suppress("ConstPropertyName") + private const val serialVersionUID: Long = 1L + } +} diff --git a/bin/main/net/thauvin/erik/mobibot/entries/EntryLink.kt b/bin/main/net/thauvin/erik/mobibot/entries/EntryLink.kt new file mode 100644 index 0000000..4a69446 --- /dev/null +++ b/bin/main/net/thauvin/erik/mobibot/entries/EntryLink.kt @@ -0,0 +1,213 @@ +/* + * EntryLink.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.thauvin.erik.mobibot.entries + +import com.rometools.rome.feed.synd.SyndCategory +import com.rometools.rome.feed.synd.SyndCategoryImpl +import net.thauvin.erik.mobibot.commands.links.LinksManager +import java.io.Serializable +import java.util.* + +/** + * The class used to store link entries. + */ +class EntryLink( + // Link's comments + val comments: MutableList<EntryComment> = mutableListOf(), + + // Tags/categories + val tags: MutableList<SyndCategory> = 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<String?> + ) : 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<SyndCategory> + ) : this(link = link, title = title, nick = nick, channel = channel, date = Date(date.time)) { + this.tags.addAll(tags) + } + + /** + * Adds a new comment + */ + fun addComment(comment: EntryComment): Int { + comments.add(comment) + return comments.lastIndex + } + + /** + * Adds a new comment. + */ + fun addComment(comment: String, nick: String): Int { + return addComment(EntryComment(comment, nick)) + } + + /** + * Deletes a specific comment. + */ + fun deleteComment(index: Int): Boolean { + if (index < comments.size) { + comments.removeAt(index) + return true + } + return false + } + + /** + * Deletes a comment. + */ + fun deleteComment(entryComment: EntryComment): Boolean { + return comments.remove(entryComment) + } + + /** + * Formats the tags. + */ + fun formatTags(sep: String, prefix: String = ""): String { + return tags.joinToString(separator = sep, prefix = prefix) { it.name } + } + + /** + * Returns a comment. + */ + fun getComment(index: Int): EntryComment = comments[index] + + /** + * Returns true if a string is contained in the link, title, or nick. + */ + fun matches(match: String?): Boolean { + return if (match.isNullOrEmpty()) { + false + } else { + link.contains(match, true) || title.contains(match, true) || nick.contains(match, true) + } + } + + /** + * Sets a comment. + */ + fun setComment(index: Int, comment: String?, nick: String?) { + if (index < comments.size && !comment.isNullOrBlank() && !nick.isNullOrBlank()) { + comments[index] = EntryComment(comment, nick) + } + } + + /** + * Sets the tags. + */ + fun setTags(tags: String) { + setTags(tags.split(LinksManager.TAG_MATCH)) + } + + /** + * Sets the tags. + */ + private fun setTags(tags: List<String?>) { + if (tags.isNotEmpty()) { + var category: SyndCategoryImpl + for (tag in tags) { + if (!tag.isNullOrBlank()) { + val t = tag.lowercase() + val mod = t[0] + if (mod == '-') { + // Don't remove the channel tag + if (channel.substring(1) != t.substring(1)) { + category = SyndCategoryImpl() + category.name = t.substring(1) + this.tags.remove(category) + } + } else { + category = SyndCategoryImpl() + if (mod == '+') { + category.name = t.substring(1) + } else { + category.name = t + } + if (!this.tags.contains(category)) { + this.tags.add(category) + } + } + } + } + } + } + + /** + * Returns a string representation of the object. + */ + override fun toString(): String { + return ("EntryLink{channel='$channel', comments=$comments, date=$date, link='$link', login='$login'," + + "nick='$nick', tags=$tags, title='$title'}") + } + + companion object { + // Serial version UID + @Suppress("ConstPropertyName") + private const val serialVersionUID: Long = 1L + } +} diff --git a/bin/main/net/thauvin/erik/mobibot/entries/FeedsManager.kt b/bin/main/net/thauvin/erik/mobibot/entries/FeedsManager.kt new file mode 100644 index 0000000..f786cb2 --- /dev/null +++ b/bin/main/net/thauvin/erik/mobibot/entries/FeedsManager.kt @@ -0,0 +1,187 @@ +/* + * FeedsManager.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.thauvin.erik.mobibot.entries + +import com.rometools.rome.feed.synd.* +import com.rometools.rome.io.FeedException +import com.rometools.rome.io.SyndFeedInput +import com.rometools.rome.io.SyndFeedOutput +import net.thauvin.erik.mobibot.Utils.toIsoLocalDate +import net.thauvin.erik.mobibot.Utils.today +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.io.IOException +import java.io.InputStreamReader +import java.io.OutputStreamWriter +import java.nio.charset.StandardCharsets +import java.nio.file.Files +import java.nio.file.Paths +import java.util.* +import kotlin.io.path.exists + +/** + * Manages the RSS feeds. + */ +class FeedsManager private constructor() { + companion object { + private val logger: Logger = LoggerFactory.getLogger(FeedsManager::class.java) + + // The file containing the current entries. + private const val CURRENT_XML = "current.xml" + + // The .xml extension. + private const val DOT_XML = ".xml" + + /** + * Loads the current feed. + */ + @JvmStatic + @Throws(IOException::class, FeedException::class) + fun loadFeed(entries: Entries, currentFile: String = CURRENT_XML): String { + entries.links.clear() + val xml = Paths.get("${entries.logsDir}${currentFile}") + var pubDate = today() + if (xml.exists()) { + val input = SyndFeedInput() + InputStreamReader( + Files.newInputStream(xml), StandardCharsets.UTF_8 + ).use { reader -> + val feed = input.build(reader) + pubDate = feed.publishedDate.toIsoLocalDate() + val items = feed.entries + var entry: EntryLink + for (i in items.indices.reversed()) { + with(items[i]) { + entry = EntryLink( + link, + title, + author.substring(author.lastIndexOf('(') + 1, author.length - 1), + entries.channel, + publishedDate, + categories + ) + var split: List<String> + for (comment in description.value.split("<br/>")) { + split = comment.split(": ".toRegex(), 2) + if (split.size == 2) { + entry.addComment(comment = split[1].trim(), nick = split[0].trim()) + } + } + } + entries.links.add(entry) + } + } + } else { + // Create an empty feed. + saveFeed(entries) + } + return pubDate + } + + /** + * Saves the feeds. + */ + @JvmStatic + fun saveFeed(entries: Entries, currentFile: String = CURRENT_XML) { + if (logger.isDebugEnabled) logger.debug("Saving the feeds...") + if (entries.logsDir.isNotBlank()) { + try { + val output = SyndFeedOutput() + val rss: SyndFeed = SyndFeedImpl() + val items: MutableList<SyndEntry> = mutableListOf() + var item: SyndEntry + OutputStreamWriter( + Files.newOutputStream(Paths.get("${entries.logsDir}${currentFile}")), StandardCharsets.UTF_8 + ).use { fw -> + with(rss) { + feedType = "rss_2.0" + title = "${entries.channel} IRC Links" + description = "Links from ${entries.ircServer} on ${entries.channel}" + if (entries.backlogs.isNotBlank()) link = entries.backlogs + publishedDate = Calendar.getInstance().time + language = "en" + } + val buff: StringBuilder = StringBuilder() + for (i in entries.links.indices.reversed()) { + with(entries.links[i]) { + buff.setLength(0) + buff.append("Posted by <b>") + .append(nick) + .append("</b> on <a href=\"irc://") + .append(entries.ircServer).append('/') + .append(channel) + .append("\"><b>") + .append(channel) + .append("</b></a>") + if (comments.isNotEmpty()) { + buff.append(" <br/><br/>") + for (j in comments.indices) { + if (j > 0) { + buff.append(" <br/>") + } + buff.append(comments[j].nick).append(": ").append(comments[j].comment) + } + } + item = SyndEntryImpl() + item.link = link + item.description = SyndContentImpl().apply { value = buff.toString() } + item.title = title + item.publishedDate = date + item.author = "${channel.removePrefix("#")}@${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() + DOT_XML + ) + ), 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/bin/main/net/thauvin/erik/mobibot/modules/AbstractModule.kt b/bin/main/net/thauvin/erik/mobibot/modules/AbstractModule.kt new file mode 100644 index 0000000..8c8e736 --- /dev/null +++ b/bin/main/net/thauvin/erik/mobibot/modules/AbstractModule.kt @@ -0,0 +1,131 @@ +/* + * AbstractModule.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.thauvin.erik.mobibot.modules + +import net.thauvin.erik.mobibot.Utils.bot +import net.thauvin.erik.mobibot.Utils.helpCmdSyntax +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<String> = mutableListOf() + + @JvmField + val help: MutableList<String> = mutableListOf() + val properties: MutableMap<String, String> = 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<String> + 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(helpCmdSyntax(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 properties.keys) { + 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/bin/main/net/thauvin/erik/mobibot/modules/Calc.kt b/bin/main/net/thauvin/erik/mobibot/modules/Calc.kt new file mode 100644 index 0000000..b7aae28 --- /dev/null +++ b/bin/main/net/thauvin/erik/mobibot/modules/Calc.kt @@ -0,0 +1,87 @@ +/* + * Calc.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.thauvin.erik.mobibot.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 <calculation>")) + } +} diff --git a/bin/main/net/thauvin/erik/mobibot/modules/ChatGpt.kt b/bin/main/net/thauvin/erik/mobibot/modules/ChatGpt.kt new file mode 100644 index 0000000..bd92332 --- /dev/null +++ b/bin/main/net/thauvin/erik/mobibot/modules/ChatGpt.kt @@ -0,0 +1,176 @@ +/* + * ChatGpt.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package net.thauvin.erik.mobibot.modules + +import net.thauvin.erik.mobibot.Constants +import net.thauvin.erik.mobibot.Utils +import net.thauvin.erik.mobibot.Utils.sendMessage +import org.apache.commons.text.WordUtils +import org.json.JSONException +import org.json.JSONObject +import org.json.JSONWriter +import org.pircbotx.hooks.types.GenericMessageEvent +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.io.IOException +import java.net.URI +import java.net.http.HttpClient +import java.net.http.HttpRequest +import java.net.http.HttpResponse + +class ChatGpt : AbstractModule() { + private val logger: Logger = LoggerFactory.getLogger(ChatGpt::class.java) + + override val name = CHATGPT_NAME + + override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) { + if (args.isNotBlank()) { + try { + val answer = chat( + args.trim(), properties[API_KEY_PROP], + properties.getOrDefault(MAX_TOKENS_PROP, "1024").toInt() + ) + if (answer.isNotBlank()) { + event.sendMessage(WordUtils.wrap(answer, 400)) + } else { + event.respond("$name is stumped.") + } + } catch (e: ModuleException) { + if (logger.isWarnEnabled) logger.warn(e.debugMessage, e) + e.message?.let { + event.respond(it) + } + } catch (e: NumberFormatException) { + if (logger.isErrorEnabled) logger.error("Invalid $MAX_TOKENS_PROP property.", e) + event.respond("The $name module is misconfigured.") + } + } else { + helpResponse(event) + } + } + + companion object { + /** + * The service name. + */ + const val CHATGPT_NAME = "ChatGPT" + + /** + * The API Key property. + */ + const val API_KEY_PROP = "chatgpt-api-key" + + /** + * The max tokens property. + */ + const val MAX_TOKENS_PROP = "chatgpt-max-tokens" + + // ChatGPT API URL + private const val API_URL = "https://api.openai.com/v1/completions" + + // ChatGPT command + private const val CHATGPT_CMD = "chatgpt" + + + @JvmStatic + @Throws(ModuleException::class) + fun chat(query: String, apiKey: String?, maxTokens: Int): String { + if (!apiKey.isNullOrEmpty()) { + val prompt = JSONWriter.valueToString("Q:$query\nA:") + val request = HttpRequest.newBuilder() + .uri(URI.create(API_URL)) + .header("Content-Type", "application/json") + .header("Authorization", "Bearer $apiKey") + .header("User-Agent", Constants.USER_AGENT) + .POST( + HttpRequest.BodyPublishers.ofString( + """{ + "model": "text-davinci-003", + "prompt": $prompt, + "temperature": 0, + "max_tokens": $maxTokens, + "top_p": 1, + "frequency_penalty": 0, + "presence_penalty": 0 + }""".trimIndent() + ) + ) + .build() + try { + val response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString()) + if (response.statusCode() == 200) { + try { + val jsonResponse = JSONObject(response.body()) + val choices = jsonResponse.getJSONArray("choices") + return choices.getJSONObject(0).getString("text").trim() + } catch (e: JSONException) { + throw ModuleException( + "$CHATGPT_CMD($query): JSON", + "A JSON error has occurred while conversing with $CHATGPT_NAME.", + e + ) + } + } else { + if (response.statusCode() == 429) { + throw ModuleException( + "$CHATGPT_CMD($query): Rate limit reached", + "Rate limit reached. Please try again later." + ) + } else { + throw IOException("HTTP Status Code: " + response.statusCode()) + } + } + } catch (e: IOException) { + throw ModuleException( + "$CHATGPT_CMD($query): IO", + "An IO error has occurred while conversing with $CHATGPT_NAME.", + e + ) + } + } else { + throw ModuleException("$CHATGPT_CMD($query)", "No $CHATGPT_NAME API key specified.") + } + } + } + + init { + commands.add(CHATGPT_CMD) + with(help) { + add("To get answers from $name:") + add(Utils.helpFormat("%c $CHATGPT_CMD <query>")) + add("For example:") + add(Utils.helpFormat("%c $CHATGPT_CMD explain quantum computing in simple terms")) + add(Utils.helpFormat("%c $CHATGPT_CMD how do I make an HTTP request in Javascript?")) + } + initProperties(API_KEY_PROP, MAX_TOKENS_PROP) + } +} diff --git a/bin/main/net/thauvin/erik/mobibot/modules/CryptoPrices.kt b/bin/main/net/thauvin/erik/mobibot/modules/CryptoPrices.kt new file mode 100644 index 0000000..d14056e --- /dev/null +++ b/bin/main/net/thauvin/erik/mobibot/modules/CryptoPrices.kt @@ -0,0 +1,159 @@ +/* + * CryptoPrices.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.thauvin.erik.mobibot.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.sendList +import net.thauvin.erik.mobibot.Utils.sendMessage +import org.json.JSONObject +import org.pircbotx.hooks.types.GenericMessageEvent +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.io.IOException + +/** + * The Cryptocurrency Prices module. + */ +class CryptoPrices : AbstractModule() { + private val logger: Logger = LoggerFactory.getLogger(CryptoPrices::class.java) + + override val name = "CryptoPrices" + + /** + * Returns the cryptocurrency market price from + * [Coinbase](https://docs.cloud.coinbase.com/sign-in-with-coinbase/docs/api-prices#get-spot-price). + */ + override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) { + if (CURRENCIES.isEmpty()) { + try { + loadCurrencies() + } catch (e: ModuleException) { + if (logger.isWarnEnabled) logger.warn(e.debugMessage, e) + } + } + + val debugMessage = "crypto($cmd $args)" + if (args == CODES_KEYWORD) { + event.sendMessage("The supported currencies are:") + event.sendList(ArrayList(CURRENCIES.keys), 10, isIndent = true) + } else 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 [${CURRENCIES[price.currency]}]") + } catch (e: CryptoException) { + if (logger.isWarnEnabled) logger.warn("$debugMessage => ${e.statusCode}", e) + e.message?.let { + event.respond(it) + } + } catch (e: IOException) { + if (logger.isErrorEnabled) logger.error(debugMessage, e) + event.respond("An IO error has occurred while retrieving the cryptocurrency market price.") + } + } else { + helpResponse(event) + } + + } + + companion object { + // Crypto command + private const val CRYPTO_CMD = "crypto" + + // Fiat Currencies + private val CURRENCIES: MutableMap<String, String> = mutableMapOf() + + // Currency codes keyword + private const val CODES_KEYWORD = "codes" + + /** + * Get current market price. + */ + @JvmStatic + fun currentPrice(args: List<String>): CryptoPrice { + return if (args.size == 2) + spotPrice(args[0], args[1]) + else + spotPrice(args[0]) + } + + /** + * For testing purposes. + */ + fun getCurrencyName(code: String): String? { + return CURRENCIES[code] + } + + /** + * Loads the Fiat currencies.. + */ + @JvmStatic + @Throws(ModuleException::class) + fun loadCurrencies() { + try { + val json = JSONObject(CryptoPrice.apiCall(listOf("currencies"))) + val data = json.getJSONArray("data") + for (i in 0 until data.length()) { + val d = data.getJSONObject(i) + CURRENCIES[d.getString("id")] = d.getString("name") + } + } catch (e: CryptoException) { + throw ModuleException( + "loadCurrencies(): CE", + "An error has occurred while retrieving the currencies table.", + e + ) + } + } + } + + init { + commands.add(CRYPTO_CMD) + with(help) { + add("To retrieve a cryptocurrency's market price:") + add(helpFormat("%c $CRYPTO_CMD <symbol> [<currency>]")) + add("For example:") + add(helpFormat("%c $CRYPTO_CMD BTC")) + add(helpFormat("%c $CRYPTO_CMD ETH EUR")) + add(helpFormat("%c $CRYPTO_CMD ETH2 GPB")) + add("To list the supported currencies:") + add(helpFormat("%c $CRYPTO_CMD $CODES_KEYWORD")) + } + loadCurrencies() + } +} diff --git a/bin/main/net/thauvin/erik/mobibot/modules/CurrencyConverter.kt b/bin/main/net/thauvin/erik/mobibot/modules/CurrencyConverter.kt new file mode 100644 index 0000000..da0efd8 --- /dev/null +++ b/bin/main/net/thauvin/erik/mobibot/modules/CurrencyConverter.kt @@ -0,0 +1,222 @@ +/* + * CurrencyConverter.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.thauvin.erik.mobibot.modules + +import net.thauvin.erik.mobibot.Utils.bot +import net.thauvin.erik.mobibot.Utils.helpCmdSyntax +import net.thauvin.erik.mobibot.Utils.helpFormat +import net.thauvin.erik.mobibot.Utils.reader +import net.thauvin.erik.mobibot.Utils.sendList +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.PublicMessage +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 +import java.text.DecimalFormat +import java.util.* + + +/** + * The CurrencyConverter module. + */ +class CurrencyConverter : AbstractModule() { + private val logger: Logger = LoggerFactory.getLogger(CurrencyConverter::class.java) + + override val name = "CurrencyConverter" + + // Reload currency codes + private fun reload(apiKey: String?) { + if (!apiKey.isNullOrEmpty() && SYMBOLS.isEmpty()) { + try { + loadSymbols(apiKey) + } catch (e: ModuleException) { + if (logger.isWarnEnabled) logger.warn(e.debugMessage, e) + } + } + } + + /** + * Converts the specified currencies. + */ + override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) { + reload(properties[API_KEY_PROP]) + + when { + SYMBOLS.isEmpty() -> { + event.respond(EMPTY_SYMBOLS_TABLE) + } + + args.matches("\\d+([,\\d]+)?(\\.\\d+)? [a-zA-Z]{3}+ (to|in) [a-zA-Z]{3}+".toRegex()) -> { + val msg = convertCurrency(properties[API_KEY_PROP], args) + event.respond(msg.msg) + if (msg.isError) { + helpResponse(event) + } + } + + args.contains(CODES_KEYWORD) -> { + event.sendMessage("The supported currency codes are:") + event.sendList(SYMBOLS.keys.toList(), 11, isIndent = true) + } + + else -> { + helpResponse(event) + } + } + } + + override fun helpResponse(event: GenericMessageEvent): Boolean { + reload(properties[API_KEY_PROP]) + + if (SYMBOLS.isEmpty()) { + event.sendMessage(EMPTY_SYMBOLS_TABLE) + } else { + val nick = event.bot().nick + event.sendMessage("To convert from one currency to another:") + event.sendMessage(helpFormat(helpCmdSyntax("%c $CURRENCY_CMD 100 USD to EUR", nick, isPrivateMsgEnabled))) + event.sendMessage( + helpFormat( + helpCmdSyntax("%c $CURRENCY_CMD 50,000 GBP to USD", nick, isPrivateMsgEnabled) + ) + ) + event.sendMessage("To list the supported currency codes:") + event.sendMessage( + helpFormat( + helpCmdSyntax("%c $CURRENCY_CMD $CODES_KEYWORD", nick, isPrivateMsgEnabled) + ) + ) + } + return true + } + + companion object { + /** + * The API Key property. + */ + const val API_KEY_PROP = "exchangerate-api-key" + + // Currency command + private const val CURRENCY_CMD = "currency" + + // Currency codes keyword + private const val CODES_KEYWORD = "codes" + + // Empty symbols table. + private const val EMPTY_SYMBOLS_TABLE = "Sorry, but the currency table is empty." + + // Currency symbols + private val SYMBOLS: TreeMap<String, String> = TreeMap() + + // Decimal format + private val DECIMAL_FORMAT = DecimalFormat("0.00#") + + /** + * Converts from a currency to another. + */ + @JvmStatic + fun convertCurrency(apiKey: String?, query: String): Message { + if (apiKey.isNullOrEmpty()) { + throw ModuleException("${CURRENCY_CMD}($query)", "No Exchange Rate API key specified.") + } + + 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 (SYMBOLS.contains(to) && SYMBOLS.contains(from)) { + try { + val amt = cmds[0].replace(",", "") + val url = URL("https://v6.exchangerate-api.com/v6/$apiKey/pair/$to/$from/$amt") + val body = url.reader().body + val json = JSONObject(body) + + if (json.getString("result") == "success") { + val result = DECIMAL_FORMAT.format(json.getDouble("conversion_result")) + PublicMessage( + "${cmds[0]} ${SYMBOLS[to]} = $result ${SYMBOLS[from]}" + ) + } else { + ErrorMessage("Sorry, an error occurred while converting the currencies.") + } + } catch (ignore: IOException) { + ErrorMessage("Sorry, an IO error occurred while converting the currencies.") + } + } else { + ErrorMessage("Sounds like monopoly money to me!") + } + } + } else { + ErrorMessage("Invalid query. Let's try again.") + } + } + + /** + * Loads the currency ISO symbols. + */ + @JvmStatic + @Throws(ModuleException::class) + fun loadSymbols(apiKey: String?) { + if (!apiKey.isNullOrEmpty()) { + try { + val url = URL("https://v6.exchangerate-api.com/v6/$apiKey/codes") + val json = JSONObject(url.reader().body) + if (json.getString("result") == "success") { + val codes = json.getJSONArray("supported_codes") + for (i in 0 until codes.length()) { + val code = codes.getJSONArray(i) + SYMBOLS[code.getString(0)] = code.getString(1) + } + } + } catch (e: IOException) { + throw ModuleException( + "loadCodes(): IOE", + "An IO error has occurred while retrieving the currencies.", + e + ) + } + } + } + } + + init { + commands.add(CURRENCY_CMD) + initProperties(API_KEY_PROP) + loadSymbols(properties[ChatGpt.API_KEY_PROP]) + } +} diff --git a/bin/main/net/thauvin/erik/mobibot/modules/Dice.kt b/bin/main/net/thauvin/erik/mobibot/modules/Dice.kt new file mode 100644 index 0000000..8420fb1 --- /dev/null +++ b/bin/main/net/thauvin/erik/mobibot/modules/Dice.kt @@ -0,0 +1,87 @@ +/* + * Dice.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.thauvin.erik.mobibot.modules + +import net.thauvin.erik.mobibot.Utils.bold +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 arg = if (args.isBlank()) "2d6" else args.trim() + val match = Regex("^([1-9]|[12]\\d|3[0-2])[dD]([1-9]|[12]\\d|3[0-2])$").find(arg) + if (match != null) { + val (dice, sides) = match.destructured + event.respond("you rolled " + roll(dice.toInt(), sides.toInt())) + } else { + helpResponse(event) + } + } + + companion object { + // Dice command + private const val DICE_CMD = "dice" + + @JvmStatic + fun roll(dice: Int, sides: Int): String { + val result = StringBuilder() + var total = 0 + + repeat(dice) { + val roll = (1..sides).random() + total += roll + + if (result.isNotEmpty()) { + result.append(" + ") + } + + result.append(roll.bold()) + } + + if (dice != 1) { + result.append(" = ${total.bold()}") + } + + return result.toString() + } + } + + init { + commands.add(DICE_CMD) + help.add("To roll 2 dice with 6 sides:") + help.add(helpFormat("%c $DICE_CMD [2d6]")) + } +} diff --git a/bin/main/net/thauvin/erik/mobibot/modules/GoogleSearch.kt b/bin/main/net/thauvin/erik/mobibot/modules/GoogleSearch.kt new file mode 100644 index 0000000..f426d1e --- /dev/null +++ b/bin/main/net/thauvin/erik/mobibot/modules/GoogleSearch.kt @@ -0,0 +1,162 @@ +/* + * GoogleSearch.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.thauvin.erik.mobibot.modules + +import net.thauvin.erik.mobibot.ReleaseInfo +import net.thauvin.erik.mobibot.Utils.capitalise +import net.thauvin.erik.mobibot.Utils.colorize +import net.thauvin.erik.mobibot.Utils.encodeUrl +import net.thauvin.erik.mobibot.Utils.helpFormat +import net.thauvin.erik.mobibot.Utils.reader +import net.thauvin.erik.mobibot.Utils.sendMessage +import net.thauvin.erik.mobibot.Utils.unescapeXml +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 : AbstractModule() { + private val logger: Logger = LoggerFactory.getLogger(GoogleSearch::class.java) + + override val name = "GoogleSearch" + + /** + * Searches Google. + */ + override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) { + if (args.isNotBlank()) { + try { + val results = searchGoogle( + args, + properties[API_KEY_PROP], + properties[CSE_KEY_PROP], + event.user.nick + ) + for (msg in results) { + if (msg.isError) { + event.respond(msg.msg.colorize(msg.color)) + } else { + 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 { + // Google API Key property + const val API_KEY_PROP = "google-api-key" + + // Google Custom Search Engine ID property + const val 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?, + quotaUser: String = ReleaseInfo.PROJECT + ): List<Message> { + 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<Message>() + if (query.isNotBlank()) { + try { + val url = URL( + "https://www.googleapis.com/customsearch/v1?key=$apiKey&cx=$cseKey" + + ""aUser=${quotaUser}&q=${query.encodeUrl()}&filter=1&num=5&alt=json" + ) + val json = JSONObject(url.reader().body) + if (json.has("items")) { + val ja = json.getJSONArray("items") + for (i in 0 until ja.length()) { + val j = ja.getJSONObject(i) + results.add(NoticeMessage(j.getString("title").unescapeXml())) + results.add(NoticeMessage(helpFormat(j.getString("link"), false), Colors.DARK_GREEN)) + } + } else if (json.has("error")) { + val error = json.getJSONObject("error") + val message = error.getString("message") + throw ModuleException("searchGoogle($query): ${error.getInt("code")} : $message", message) + } 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 <query>")) + initProperties(API_KEY_PROP, CSE_KEY_PROP) + } +} diff --git a/bin/main/net/thauvin/erik/mobibot/modules/Joke.kt b/bin/main/net/thauvin/erik/mobibot/modules/Joke.kt new file mode 100644 index 0000000..2760fa7 --- /dev/null +++ b/bin/main/net/thauvin/erik/mobibot/modules/Joke.kt @@ -0,0 +1,105 @@ +/* + * Joke.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.thauvin.erik.mobibot.modules + +import net.thauvin.erik.jokeapi.exceptions.HttpErrorException +import net.thauvin.erik.jokeapi.exceptions.JokeException +import net.thauvin.erik.jokeapi.joke +import net.thauvin.erik.jokeapi.models.Type +import net.thauvin.erik.mobibot.Utils.bot +import net.thauvin.erik.mobibot.Utils.colorize +import net.thauvin.erik.mobibot.Utils.helpFormat +import net.thauvin.erik.mobibot.msg.Message +import net.thauvin.erik.mobibot.msg.PublicMessage +import org.json.JSONException +import org.pircbotx.Colors +import org.pircbotx.hooks.types.GenericMessageEvent +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.io.IOException + +/** + * The Joke module. + */ +class Joke : AbstractModule() { + private val logger: Logger = LoggerFactory.getLogger(Joke::class.java) + + override val name = "Joke" + + /** + * Returns a random joke from [JokeAPI](https://v2.jokeapi.dev/). + */ + override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) { + with(event.bot()) { + try { + randomJoke().forEach { + sendIRC().notice(channel, it.msg.colorize(it.color)) + } + } catch (e: ModuleException) { + if (logger.isWarnEnabled) logger.warn(e.debugMessage, e) + e.message?.let { + event.respond(it) + } + } + } + } + + companion object { + // Joke command + private const val JOKE_CMD = "joke" + + /** + * Retrieves a random joke. + */ + @JvmStatic + @Throws(ModuleException::class) + fun randomJoke(): List<Message> { + return try { + val joke = joke(safe = true, type = Type.SINGLE, splitNewLine = true) + joke.joke.map { PublicMessage(it, Colors.CYAN) } + } catch (e: JokeException) { + throw ModuleException("randomJoke(): ${e.additionalInfo}", e.message, e) + } catch (e: HttpErrorException) { + throw ModuleException("randomJoke(): HTTP: ${e.statusCode}", e.message, e) + } catch (e: IOException) { + throw ModuleException("randomJoke(): IOE", "An IO error has occurred retrieving a random joke.", e) + } catch (e: JSONException) { + throw ModuleException("randomJoke(): JSON", "A parsing error has occurred retrieving a random joke.", e) + } + } + } + + init { + commands.add(JOKE_CMD) + help.add("To display a random joke:") + help.add(helpFormat("%c $JOKE_CMD")) + } +} diff --git a/bin/main/net/thauvin/erik/mobibot/modules/Lookup.kt b/bin/main/net/thauvin/erik/mobibot/modules/Lookup.kt new file mode 100644 index 0000000..9ab2ead --- /dev/null +++ b/bin/main/net/thauvin/erik/mobibot/modules/Lookup.kt @@ -0,0 +1,171 @@ +/* + * Lookup.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.thauvin.erik.mobibot.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<String> { + return whois(query, WHOIS_HOST) + } + + /** + * Performs a whois IP query. + */ + @JvmStatic + @Throws(IOException::class) + fun whois(query: String, host: String): List<String> { + val whoisClient = WhoisClient() + val lines: List<String> + 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 <ip address or hostname>")) + } +} diff --git a/bin/main/net/thauvin/erik/mobibot/modules/Mastodon.kt b/bin/main/net/thauvin/erik/mobibot/modules/Mastodon.kt new file mode 100644 index 0000000..3be3a5f --- /dev/null +++ b/bin/main/net/thauvin/erik/mobibot/modules/Mastodon.kt @@ -0,0 +1,149 @@ +/* + * Mastodon.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package net.thauvin.erik.mobibot.modules + +import net.thauvin.erik.mobibot.Utils +import net.thauvin.erik.mobibot.Utils.prefixIfMissing +import net.thauvin.erik.mobibot.entries.EntryLink +import net.thauvin.erik.mobibot.social.SocialModule +import org.json.JSONException +import org.json.JSONObject +import org.json.JSONWriter +import java.io.IOException +import java.net.URI +import java.net.http.HttpClient +import java.net.http.HttpRequest +import java.net.http.HttpResponse + +class Mastodon : SocialModule() { + override val name = "Mastodon" + + override val handle: String? + get() = properties[HANDLE_PROP] + + override val isAutoPost: Boolean + get() = isEnabled && properties[AUTO_POST_PROP].toBoolean() + + override val isValidProperties: Boolean + get() = !(properties[INSTANCE_PROP].isNullOrBlank() || properties[ACCESS_TOKEN_PROP].isNullOrBlank()) + + /** + * Formats the entry for posting. + */ + override fun formatEntry(entry: EntryLink): String { + return "${entry.title} (via ${entry.nick} on ${entry.channel})${formatTags(entry)}\n\n${entry.link}" + } + + private fun formatTags(entry: EntryLink): String { + return entry.tags.filter { !it.name.equals(entry.channel.removePrefix("#"), true) } + .joinToString(separator = " ", prefix = "\n\n") { "#${it.name}" } + } + + /** + * Posts on Mastodon. + */ + @Throws(ModuleException::class) + override fun post(message: String, isDm: Boolean): String { + return toot( + apiKey = properties[ACCESS_TOKEN_PROP], + instance = properties[INSTANCE_PROP], + handle = handle, + message = message, + isDm = isDm + ) + } + + companion object { + // Property keys + const val ACCESS_TOKEN_PROP = "mastodon-access-token" + const val AUTO_POST_PROP = "mastodon-auto-post" + const val HANDLE_PROP = "mastodon-handle" + const val INSTANCE_PROP = "mastodon-instance" + + private const val MASTODON_CMD = "mastodon" + private const val TOOT_CMD = "toot" + + /** + * Post on Mastodon. + */ + @JvmStatic + @Throws(ModuleException::class) + fun toot(apiKey: String?, instance: String?, handle: String?, message: String, isDm: Boolean): String { + val request = HttpRequest.newBuilder() + .uri(URI.create("https://$instance/api/v1/statuses")) + .header("Content-Type", "application/json") + .header("Authorization", "Bearer $apiKey") + .POST( + HttpRequest.BodyPublishers.ofString( + JSONWriter.valueToString( + if (isDm) { + mapOf("status" to "${handle?.prefixIfMissing('@')} $message", "visibility" to "direct") + } else { + mapOf("status" to message) + } + ) + ) + ) + .build() + try { + val response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString()) + if (response.statusCode() == 200) { + return try { + val jsonResponse = JSONObject(response.body()) + if (isDm) { + jsonResponse.getString("content") + } else { + "Your message was posted to ${jsonResponse.getString("url")}" + } + } catch (e: JSONException) { + throw ModuleException("mastodonPost($message)", "A JSON error has occurred: ${e.message}", e) + } + } else { + throw IOException("Status Code: " + response.statusCode()) + } + } catch (e: IOException) { + throw ModuleException("mastodonPost($message)", "An IO error has occurred: ${e.message}", e) + } catch (e: InterruptedException) { + throw ModuleException("mastodonPost($message)", "An error has occurred: ${e.message}", e) + } + } + } + + init { + commands.add(MASTODON_CMD) + commands.add(TOOT_CMD) + help.add("To toot on Mastodon:") + help.add(Utils.helpFormat("%c $TOOT_CMD <message>")) + properties[AUTO_POST_PROP] = "false" + initProperties(ACCESS_TOKEN_PROP, HANDLE_PROP, INSTANCE_PROP) + } +} diff --git a/bin/main/net/thauvin/erik/mobibot/modules/ModuleException.kt b/bin/main/net/thauvin/erik/mobibot/modules/ModuleException.kt new file mode 100644 index 0000000..a7416c2 --- /dev/null +++ b/bin/main/net/thauvin/erik/mobibot/modules/ModuleException.kt @@ -0,0 +1,45 @@ +/* + * ModuleException.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.thauvin.erik.mobibot.modules + +/** + * The `ModuleException` class. + */ +class ModuleException @JvmOverloads constructor( + val debugMessage: String, + message: String? = null, + cause: Throwable? = null +) : Exception(message, cause) { + companion object { + @Suppress("ConstPropertyName") + private const val serialVersionUID = 1L + } +} diff --git a/bin/main/net/thauvin/erik/mobibot/modules/Ping.kt b/bin/main/net/thauvin/erik/mobibot/modules/Ping.kt new file mode 100644 index 0000000..944dbc1 --- /dev/null +++ b/bin/main/net/thauvin/erik/mobibot/modules/Ping.kt @@ -0,0 +1,83 @@ +/* + * Ping.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.thauvin.erik.mobibot.modules + +import net.thauvin.erik.mobibot.Utils.bot +import net.thauvin.erik.mobibot.Utils.helpFormat +import org.pircbotx.hooks.types.GenericMessageEvent + +/** + * The Ping module. + */ +class Ping : AbstractModule() { + override val name = "Ping" + + override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) { + event.bot().sendIRC().action(channel, randomPing()) + } + + 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!" + ) + + @JvmStatic + fun randomPing(): String { + return PINGS[PINGS.indices.random()] + } + + /** + * The ping command. + */ + private const val PING_CMD = "ping" + } + + init { + commands.add(PING_CMD) + help.add("To ping the bot:") + help.add(helpFormat("%c $PING_CMD")) + } +} diff --git a/bin/main/net/thauvin/erik/mobibot/modules/RockPaperScissors.kt b/bin/main/net/thauvin/erik/mobibot/modules/RockPaperScissors.kt new file mode 100644 index 0000000..a299d8d --- /dev/null +++ b/bin/main/net/thauvin/erik/mobibot/modules/RockPaperScissors.kt @@ -0,0 +1,114 @@ +/* + * RockPaperScissors.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package net.thauvin.erik.mobibot.modules + +import net.thauvin.erik.mobibot.Utils.bold +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.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()}" + ) + ) + } + } + + enum class Hands(val action: String) { + ROCK("crushes") { + override fun beats(hand: Hands): Boolean { + return hand == SCISSORS + } + }, + PAPER("covers") { + override fun beats(hand: Hands): Boolean { + return hand == ROCK + } + }, + SCISSORS("cuts") { + override fun beats(hand: Hands): Boolean { + return hand == PAPER + } + }; + + abstract fun beats(hand: Hands): Boolean + } + + companion object { + // For testing. + fun winLoseOrDraw(player: String, bot: String): String { + val hand = Hands.valueOf(player.uppercase()) + val botHand = Hands.valueOf(bot.uppercase()) + + return when { + hand == botHand -> "draw" + hand.beats(botHand) -> "win" + else -> "lose" + } + } + } + + override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) { + val hand = Hands.valueOf(cmd.uppercase()) + val botHand = Hands.entries[(0..Hands.entries.size).random()] + when { + hand == botHand -> { + event.respond("${hand.name} vs. ${botHand.name} » You ${"tie".bold()}.") + } + + hand.beats(botHand) -> { + event.respond("${hand.name.bold()} ${hand.action} ${botHand.name} » You ${"win".bold()}!") + } + + else -> { + event.respond("${botHand.name.bold()} ${botHand.action} ${hand.name} » You ${"lose".bold()}!") + } + } + } +} diff --git a/bin/main/net/thauvin/erik/mobibot/modules/StockQuote.kt b/bin/main/net/thauvin/erik/mobibot/modules/StockQuote.kt new file mode 100644 index 0000000..dcae5e7 --- /dev/null +++ b/bin/main/net/thauvin/erik/mobibot/modules/StockQuote.kt @@ -0,0 +1,236 @@ +/* + * StockQuote.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.thauvin.erik.mobibot.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.reader +import net.thauvin.erik.mobibot.Utils.sendMessage +import net.thauvin.erik.mobibot.Utils.unescapeXml +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 : AbstractModule() { + private val logger: Logger = LoggerFactory.getLogger(StockQuote::class.java) + + override val name = "StockQuote" + + /** + * Returns the specified stock quote from Alpha Vantage. + */ + override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) { + if (args.isNotBlank()) { + try { + val messages = getQuote(args, properties[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.respond(it) + } + } + } else { + helpResponse(event) + } + } + + companion object { + /** + * The API property key. + */ + const val API_KEY_PROP = "alphavantage-api-key" + + /** + * The Invalid Symbol error string. + */ + const val INVALID_SYMBOL = "Invalid symbol." + + // API URL + private const val API_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, info.unescapeXml()) + } + } catch (ignore: JSONException) { + // Do nothing + } + try { + var error = json.getString("Note") + if (error.isNotEmpty()) { + throw ModuleException(debugMessage, error.unescapeXml()) + } + error = json.getString("Error Message") + if (error.isNotEmpty()) { + throw ModuleException(debugMessage, error.unescapeXml()) + } + } 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<Message> { + if (apiKey.isNullOrBlank()) { + throw ModuleException( + "${StockQuote::class.java.name} is disabled.", + "${STOCK_CMD.capitalise()} is disabled. The API key is missing." + ) + } + val messages = mutableListOf<Message>() + if (symbol.isNotBlank()) { + val debugMessage = "getQuote($symbol)" + var response: String + try { + with(messages) { + // Search for symbol/keywords + response = URL( + "${API_URL}SYMBOL_SEARCH&keywords=" + symbol.encodeUrl() + "&apikey=" + + apiKey.encodeUrl() + ).reader().body + 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 = URL( + "${API_URL}GLOBAL_QUOTE&symbol=" + + symbolInfo.getString("1. symbol").encodeUrl() + "&apikey=" + + apiKey.encodeUrl() + ).reader().body + json = getJsonResponse(response, debugMessage) + val quote = json.getJSONObject("Global Quote") + if (quote.isEmpty) { + add(ErrorMessage(INVALID_SYMBOL)) + } else { + + add( + PublicMessage( + "Symbol: " + quote.getString("01. symbol").unescapeXml() + + " [" + symbolInfo.getString("2. name").unescapeXml() + ']' + ) + ) + + val pad = 10 + + add( + PublicMessage( + "Price:".padEnd(pad).prependIndent() + + quote.getString("05. price").unescapeXml() + ) + ) + add( + PublicMessage( + "Previous:".padEnd(pad).prependIndent() + + quote.getString("08. previous close").unescapeXml() + ) + ) + + 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() + + quote.getString(it.second).unescapeXml() + ) + ) + } + + add( + NoticeMessage( + "Change:".padEnd(pad).prependIndent() + + quote.getString("09. change").unescapeXml() + + " [" + quote.getString("10. change percent").unescapeXml() + ']' + ) + ) + } + } + } + } 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 <symbol|keywords>")) + initProperties(API_KEY_PROP) + } +} diff --git a/bin/main/net/thauvin/erik/mobibot/modules/War.class b/bin/main/net/thauvin/erik/mobibot/modules/War.class new file mode 100644 index 0000000000000000000000000000000000000000..6c36e541b639c5b04b624194b3b2df6b3d11ba55 GIT binary patch literal 2452 zcmeHJUvm>T5MMb??6`r@5CVjMYDuZxQXf#BNMXjcP07@D7&{5m;i02DE4IR&<VrfX zdE`?u)6T$qzXso<)79MtNOEL4Q<&*PAFN&Jw7b7u?N9QbfBpUv0Pe%LB`6SBliYfC z%;Kq#9@pZT7b#!(%Ay>`AvfL=rb|#HaAuE9nHMrS@;3cF9#{gi&Cd4s0|IBCcHVv* zSS{peoj|#@(dcgWTDW2EQM=LKa!S9^Yi)K3Tuv<v4`MCszBh0R?=v}65!%iT9yeNT zG$^*(%^wLYKg|tMdeZ3s-0ZdpEaf3M)l|}0ChZOjV_t?O0yDodjqbk_QrHJLt=4uh zNK*}Y3C<H(e49sGYo*I@krZBUOU;c6r+i2Q6-`9QtWc6pwDLn9nR~P{zcuDmL=&Yg z#fsLmiq>64>sdwX+g5~z224t2sn6+w(|>E-@Qu|hu-)Xuasp7RD5G|N!YlKbK!jD! z$1&#NB*(ro|K1NL=M-@}Rzo`Cw#On~tx>g`z@XSvG>VO-YRa`1Lr#@;^}}*<X%Vc6 zCtN$kH1(0zDD#))(C21?2h6K!%iW|$<qurq0R&xc3>)$G6kUHP2)+u3S=<^GhQ@Np zcZI|b*~LsOc0&Rl^E`z7KWkQj6}YqjXW=q|bJgQyax_`&V1m6k^10q(*!hliDq!Ib z)56WO#iAVxL*Pc|pIj}-^-|Uz$nt9Kcw_>X`mwYk;u~@*jKDw*$Do9R80UIN>5;d` zt*@Do#!Kjxe)M|E_Tn&HFT)K2zno0t0~UY4o+WVYDD*9C=|k`rk2EuZQBL`fmnCP# z)nOuZfVcN=-dL$;#&b<*yc`~^y8~T2itaJf$WzPpI}poolaS{p*Y3>~_&-5MAL%+E zP#LIL2mGOM%q!!~Qg@FP(se@ycnu0;@qT+GxMn&S@0Z{<fzyYd1_BFRVGKUct_=;` z27CeW&p-)QVHW04TSYyU<xI{)C0YL@n=inrWPTbJ(fSPP1^5)b;J*M^#_=4k+)d^H zl|NwRr=<%oVD(k9?+T7{xCh{KxQgRKqF3M=P@EC?4A*DiI_hrM8p`9|zR2;`&GGgn zT9<(=L*j=(6>8}FD_p%;%$%no-W?CZ&EmEc{MR|*w%-%(Dil}H5OJ?yj!qO(fi-r? U%)mD|7N8CuN`O1~b69Tu4Ue7BiU0rr literal 0 HcmV?d00001 diff --git a/bin/main/net/thauvin/erik/mobibot/modules/Weather2.kt b/bin/main/net/thauvin/erik/mobibot/modules/Weather2.kt new file mode 100644 index 0000000..80a06fa --- /dev/null +++ b/bin/main/net/thauvin/erik/mobibot/modules/Weather2.kt @@ -0,0 +1,250 @@ +/* + * Weather2.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.thauvin.erik.mobibot.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 : AbstractModule() { + private val logger: Logger = LoggerFactory.getLogger(Weather2::class.java) + + override val name = "Weather" + + /** + * Fetches the weather data from a specific city. + */ + override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) { + if (args.isNotBlank()) { + try { + val messages = getWeather(args, properties[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 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<Int, Int> { + 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.entries) { + 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<Message> { + 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<Message>() + 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()) { + temp?.let { t -> + val (f, c) = ftoC(t) + messages.add(PublicMessage("Temperature: ${f}°F, ${c}°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=" + + "$city,${code.uppercase()}".encodeUrl(), + 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<Int, Int> { + 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 <city> [, <country code>]")) + 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(API_KEY_PROP) + } +} diff --git a/bin/main/net/thauvin/erik/mobibot/modules/WolframAlpha.kt b/bin/main/net/thauvin/erik/mobibot/modules/WolframAlpha.kt new file mode 100644 index 0000000..a72efab --- /dev/null +++ b/bin/main/net/thauvin/erik/mobibot/modules/WolframAlpha.kt @@ -0,0 +1,142 @@ +/* + * WolframAlpha.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package net.thauvin.erik.mobibot.modules + +import net.thauvin.erik.mobibot.Utils +import net.thauvin.erik.mobibot.Utils.encodeUrl +import net.thauvin.erik.mobibot.Utils.isHttpSuccess +import net.thauvin.erik.mobibot.Utils.reader +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 +import java.net.URL + +class WolframAlpha : AbstractModule() { + private val logger: Logger = LoggerFactory.getLogger(WolframAlpha::class.java) + + override val name = "WolframAlpha" + + private fun getUnits(unit: String?): String { + return if (unit?.lowercase() == METRIC) { + METRIC + } else { + IMPERIAL + } + } + + override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) { + if (args.isNotBlank()) { + try { + val query = args.trim().split("units=", limit = 2, ignoreCase = true) + event.sendMessage( + queryWolfram( + query[0].trim(), + units = if (query.size == 2) { + getUnits(query[1].trim()) + } else { + getUnits(properties[UNITS_PROP]) + }, + appId = properties[APPID_KEY_PROP] + ) + ) + } catch (e: ModuleException) { + if (logger.isWarnEnabled) logger.warn(e.debugMessage, e) + e.message?.let { + event.respond(it) + } + } + } else { + helpResponse(event) + } + } + + companion object { + /** + * The Wolfram Alpha API Key property. + */ + const val APPID_KEY_PROP = "wolfram-appid" + + /** + * The Wolfram units properties + */ + const val UNITS_PROP = "wolfram-units" + + const val METRIC = "metric" + const val IMPERIAL = "imperial" + + // Wolfram command + private const val WOLFRAM_CMD = "wolfram" + + // Wolfram Alpha API URL + private const val API_URL = "http://api.wolframalpha.com/v1/spoken?appid=" + + @JvmStatic + @Throws(ModuleException::class) + fun queryWolfram(query: String, units: String = IMPERIAL, appId: String?): String { + if (!appId.isNullOrEmpty()) { + try { + val urlReader = URL("${API_URL}${appId}&units=${units}&i=" + query.encodeUrl()).reader() + if (urlReader.responseCode.isHttpSuccess()) { + return urlReader.body + } else { + throw ModuleException( + "wolfram($query): ${urlReader.responseCode} : ${urlReader.body} ", + urlReader.body.ifEmpty { + "Looks like Wolfram Alpha isn't able to answer that. (${urlReader.responseCode})" + } + ) + } + } catch (ioe: IOException) { + throw ModuleException( + "wolfram($query): IOE", "An IO Error occurred while querying Wolfram Alpha.", ioe + ) + } + } else { + throw ModuleException("wolfram($query): No API Key", "No Wolfram Alpha API key specified.") + } + } + } + + init { + commands.add(WOLFRAM_CMD) + with(help) { + add("To get answers from Wolfram Alpha:") + add(Utils.helpFormat("%c $WOLFRAM_CMD <query> [units=(${METRIC}|${IMPERIAL})]")) + add("For example:") + add(Utils.helpFormat("%c $WOLFRAM_CMD days until christmas")) + add(Utils.helpFormat("%c $WOLFRAM_CMD distance earth moon units=metric")) + } + initProperties(APPID_KEY_PROP, UNITS_PROP) + } +} diff --git a/bin/main/net/thauvin/erik/mobibot/modules/WorldTime.kt b/bin/main/net/thauvin/erik/mobibot/modules/WorldTime.kt new file mode 100644 index 0000000..18072bc --- /dev/null +++ b/bin/main/net/thauvin/erik/mobibot/modules/WorldTime.kt @@ -0,0 +1,390 @@ +/* + * WorldTime.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.thauvin.erik.mobibot.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<String, String> { + 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 [<country code or zone>]")) + add("For a listing of the supported countries/zones:") + add(helpFormat("%c $TIME_CMD $ZONES_ARGS")) + } + commands.add(TIME_CMD) + } +} diff --git a/bin/main/net/thauvin/erik/mobibot/msg/ErrorMessage.kt b/bin/main/net/thauvin/erik/mobibot/msg/ErrorMessage.kt new file mode 100644 index 0000000..0607936 --- /dev/null +++ b/bin/main/net/thauvin/erik/mobibot/msg/ErrorMessage.kt @@ -0,0 +1,37 @@ +/* + * ErrorMessage.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.thauvin.erik.mobibot.msg + +/** + * The `ErrorMessage` class. + */ +class ErrorMessage @JvmOverloads constructor(msg: String, color: String = DEFAULT_COLOR) : + Message(msg, color, isError = true) diff --git a/bin/main/net/thauvin/erik/mobibot/msg/Message.kt b/bin/main/net/thauvin/erik/mobibot/msg/Message.kt new file mode 100644 index 0000000..23a33b9 --- /dev/null +++ b/bin/main/net/thauvin/erik/mobibot/msg/Message.kt @@ -0,0 +1,65 @@ +/* + * Message.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.thauvin.erik.mobibot.msg + +import net.thauvin.erik.semver.Constants + +/** + * The `Message` class. + */ +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 + } + + init { + if (isError) { + isNotice = true + } + } + + /** Error flag. */ + var isError = isError + set(value) { + if (value) isNotice = true + field = value + } + + override fun toString(): String { + return "Message(color='$color', isError=$isError, isNotice=$isNotice, isPrivate=$isPrivate, msg='$msg')" + } +} diff --git a/bin/main/net/thauvin/erik/mobibot/msg/NoticeMessage.kt b/bin/main/net/thauvin/erik/mobibot/msg/NoticeMessage.kt new file mode 100644 index 0000000..037d504 --- /dev/null +++ b/bin/main/net/thauvin/erik/mobibot/msg/NoticeMessage.kt @@ -0,0 +1,38 @@ +/* + * NoticeMessage.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.thauvin.erik.mobibot.msg + +/** + * The `NoticeMessage` class. + */ +class NoticeMessage @JvmOverloads constructor(msg: String, color: String = DEFAULT_COLOR) : + Message(msg, color, isNotice = true) + diff --git a/bin/main/net/thauvin/erik/mobibot/msg/PrivateMessage.kt b/bin/main/net/thauvin/erik/mobibot/msg/PrivateMessage.kt new file mode 100644 index 0000000..b424fdf --- /dev/null +++ b/bin/main/net/thauvin/erik/mobibot/msg/PrivateMessage.kt @@ -0,0 +1,37 @@ +/* + * PrivateMessage.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.thauvin.erik.mobibot.msg + +/** + * The `PrivateMessage` class. + */ +class PrivateMessage @JvmOverloads constructor(msg: String, color: String = DEFAULT_COLOR) : + Message(msg, color, isPrivate = true) diff --git a/bin/main/net/thauvin/erik/mobibot/msg/PublicMessage.kt b/bin/main/net/thauvin/erik/mobibot/msg/PublicMessage.kt new file mode 100644 index 0000000..9c5e088 --- /dev/null +++ b/bin/main/net/thauvin/erik/mobibot/msg/PublicMessage.kt @@ -0,0 +1,36 @@ +/* + * PublicMessage.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.thauvin.erik.mobibot.msg + +/** + * The `PublicMessage` class. + */ +class PublicMessage @JvmOverloads constructor(msg: String, color: String = DEFAULT_COLOR) : Message(msg, color) diff --git a/bin/main/net/thauvin/erik/mobibot/social/SocialManager.kt b/bin/main/net/thauvin/erik/mobibot/social/SocialManager.kt new file mode 100644 index 0000000..91f2dd9 --- /dev/null +++ b/bin/main/net/thauvin/erik/mobibot/social/SocialManager.kt @@ -0,0 +1,116 @@ +/* + * SocialManager.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package net.thauvin.erik.mobibot.social + +import net.thauvin.erik.mobibot.Addons +import net.thauvin.erik.mobibot.Constants +import net.thauvin.erik.mobibot.entries.EntriesUtils.toLinkLabel +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.util.* + +/** + * Social Manager. + */ +class SocialManager { + private val entries: MutableSet<Int> = HashSet() + private val logger: Logger = LoggerFactory.getLogger(SocialManager::class.java) + private val modules = ArrayList<SocialModule>() + private val timer = Timer(true) + + /** + * Adds social modules. + */ + fun add(addons: Addons, vararg modules: SocialModule) { + modules.forEach { + if (addons.add(it)) { + this.modules.add(it) + } + } + } + + /** + * Returns the number of entries. + */ + fun entriesCount(): Int = entries.size + + /** + * Sends a social notification (dm, etc.) + */ + fun notification(msg: String) { + modules.forEach { + it.notification(msg) + } + } + + /** + * Posts to social media. + */ + fun postEntry(index: Int) { + if (entries.contains(index)) { + modules.forEach { + it.postEntry(index) + } + entries.remove(index) + } + } + + /** + * Queues an entry for posting to social media. + */ + fun queueEntry(index: Int) { + if (modules.isNotEmpty()) { + entries.add(index) + if (logger.isDebugEnabled) { + logger.debug("Scheduling {} for posting on social media.", index.toLinkLabel()) + } + timer.schedule(SocialTimer(this, index), Constants.TIMER_DELAY * 60L * 1000L) + } + } + + /** + * Removes entries from queue. + */ + fun removeEntry(index: Int) { + entries.remove(index) + } + + /** + * Posts all entries on shutdown. + */ + fun shutdown() { + timer.cancel() + entries.forEach { + postEntry(it) + } + } +} diff --git a/bin/main/net/thauvin/erik/mobibot/social/SocialModule.kt b/bin/main/net/thauvin/erik/mobibot/social/SocialModule.kt new file mode 100644 index 0000000..b594670 --- /dev/null +++ b/bin/main/net/thauvin/erik/mobibot/social/SocialModule.kt @@ -0,0 +1,96 @@ +/* + * SocialModule.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package net.thauvin.erik.mobibot.social + +import net.thauvin.erik.mobibot.commands.links.LinksManager +import net.thauvin.erik.mobibot.entries.EntriesUtils.toLinkLabel +import net.thauvin.erik.mobibot.entries.EntryLink +import net.thauvin.erik.mobibot.modules.AbstractModule +import net.thauvin.erik.mobibot.modules.ModuleException +import org.pircbotx.hooks.types.GenericMessageEvent +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +abstract class SocialModule : AbstractModule() { + private val logger: Logger = LoggerFactory.getLogger(SocialManager::class.java) + + abstract val handle: String? + abstract val isAutoPost: Boolean + + abstract fun formatEntry(entry: EntryLink): String + + /** + * Sends a DM. + */ + fun notification(msg: String) { + if (isEnabled && !handle.isNullOrBlank()) { + try { + post(message = msg, isDm = true) + if (logger.isDebugEnabled) logger.debug("Notified $handle on $name: $msg") + } catch (e: ModuleException) { + if (logger.isWarnEnabled) logger.warn("Failed to notify $handle on $name: $msg", e) + } + } + } + + abstract fun post(message: String, isDm: Boolean): String + + /** + * Post entry to social media. + */ + fun postEntry(index: Int) { + if (isAutoPost && LinksManager.entries.links.size >= index) { + try { + if (logger.isDebugEnabled) { + logger.debug("Posting {} to $name.", index.toLinkLabel()) + } + post(message = formatEntry(LinksManager.entries.links[index]), isDm = false) + } catch (e: ModuleException) { + if (logger.isWarnEnabled) logger.warn( + "Failed to post entry ${index.toLinkLabel()} on $name.", + e + ) + } + } + } + + override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) { + try { + event.respond(post("$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) + } + } + } +} diff --git a/bin/main/net/thauvin/erik/mobibot/social/SocialTimer.kt b/bin/main/net/thauvin/erik/mobibot/social/SocialTimer.kt new file mode 100644 index 0000000..3fd315e --- /dev/null +++ b/bin/main/net/thauvin/erik/mobibot/social/SocialTimer.kt @@ -0,0 +1,40 @@ +/* + * SocialTimer.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package net.thauvin.erik.mobibot.social + +import java.util.* + +class SocialTimer(private var socialManager: SocialManager, private var index: Int) : TimerTask() { + override fun run() { + socialManager.postEntry(index) + } +} diff --git a/bin/test/current.xml b/bin/test/current.xml new file mode 100644 index 0000000..535a400 --- /dev/null +++ b/bin/test/current.xml @@ -0,0 +1,67 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + ~ current.xml + ~ + ~ Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + ~ + ~ Redistribution and use in source and binary forms, with or without + ~ modification, are permitted provided that the following conditions are met: + ~ + ~ Redistributions of source code must retain the above copyright notice, this + ~ list of conditions and the following disclaimer. + ~ + ~ Redistributions in binary form must reproduce the above copyright notice, + ~ this list of conditions and the following disclaimer in the documentation + ~ and/or other materials provided with the distribution. + ~ + ~ Neither the name of this project nor the names of its contributors may be + ~ used to endorse or promote products derived from this software without + ~ specific prior written permission. + ~ + ~ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + ~ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + ~ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + ~ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + ~ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + ~ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + ~ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + ~ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + ~ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + ~ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + --> + +<rss xmlns:dc="http://purl.org/dc/elements/1.1/" version="2.0"> + <channel> + <title>#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/bin/test/net/thauvin/erik/mobibot/AddonsTest.kt b/bin/test/net/thauvin/erik/mobibot/AddonsTest.kt new file mode 100644 index 0000000..afb8ce6 --- /dev/null +++ b/bin/test/net/thauvin/erik/mobibot/AddonsTest.kt @@ -0,0 +1,86 @@ +/* + * AddonsTest.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package net.thauvin.erik.mobibot + +import assertk.assertThat +import assertk.assertions.containsExactly +import assertk.assertions.isEqualTo +import assertk.assertions.size +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.* +import kotlin.test.Test +import java.util.* + +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(War()) + addons.add(Dice()) + addons.add(Lookup()) + assertThat(addons::modules).size().isEqualTo(2) + assertThat(addons.names.modules, "names.modules").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().isEqualTo(3) + + assertThat(addons.names.ops, "names.ops").containsExactly("cycle") + + assertThat(addons.names.commands, "names.command").containsExactly( + "joke", + "rock", + "paper", + "scissors", + "ignore" + ) + } +} diff --git a/bin/test/net/thauvin/erik/mobibot/ExceptionSanitizer.kt b/bin/test/net/thauvin/erik/mobibot/ExceptionSanitizer.kt new file mode 100644 index 0000000..a3994ec --- /dev/null +++ b/bin/test/net/thauvin/erik/mobibot/ExceptionSanitizer.kt @@ -0,0 +1,60 @@ +/* + * ExceptionSanitizer.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package net.thauvin.erik.mobibot + +import net.thauvin.erik.mobibot.Utils.obfuscate +import net.thauvin.erik.mobibot.Utils.replaceEach +import net.thauvin.erik.mobibot.modules.ModuleException + +object ExceptionSanitizer { + /** + * Returns a sanitized exception to avoid displaying api keys, etc. in CI logs. + */ + 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/bin/test/net/thauvin/erik/mobibot/FeedReaderTest.kt b/bin/test/net/thauvin/erik/mobibot/FeedReaderTest.kt new file mode 100644 index 0000000..6e5fa8f --- /dev/null +++ b/bin/test/net/thauvin/erik/mobibot/FeedReaderTest.kt @@ -0,0 +1,78 @@ +/* + * FeedReaderTest.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.thauvin.erik.mobibot + +import assertk.all +import assertk.assertFailure +import assertk.assertThat +import assertk.assertions.* +import com.rometools.rome.io.FeedException +import net.thauvin.erik.mobibot.FeedReader.Companion.readFeed +import net.thauvin.erik.mobibot.msg.Message +import kotlin.test.Test +import java.io.IOException +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, "messages").all { + size().isEqualTo(10) + index(1).prop(Message::msg).contains("erik.thauvin.net") + } + + messages = readFeed("https://lorem-rss.herokuapp.com/feed?length=0") + assertThat(messages, "messages").index(0).prop(Message::msg).contains("nothing") + + messages = readFeed("https://lorem-rss.herokuapp.com/feed?length=84", 42) + assertThat(messages, "messages").size().isEqualTo(84) + messages.forEachIndexed { i, m -> + if (i % 2 == 0) { + assertThat(m, "messages($i)").prop(Message::msg).startsWith("Lorem ipsum") + } else { + assertThat(m, "messages($i)").prop(Message::msg).contains("http://example.com/test/") + } + } + + assertFailure { readFeed("blah") }.isInstanceOf(MalformedURLException::class.java) + + assertFailure { readFeed("https://www.example.com") }.isInstanceOf(FeedException::class.java) + + assertFailure { readFeed("https://www.thauvin.net/foo") }.isInstanceOf(IOException::class.java) + + assertFailure { readFeed("https://www.examplesfoo.com/") }.isInstanceOf(UnknownHostException::class.java) + } +} diff --git a/bin/test/net/thauvin/erik/mobibot/LocalProperties.kt b/bin/test/net/thauvin/erik/mobibot/LocalProperties.kt new file mode 100644 index 0000000..1384a72 --- /dev/null +++ b/bin/test/net/thauvin/erik/mobibot/LocalProperties.kt @@ -0,0 +1,85 @@ +/* + * LocalProperties.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.thauvin.erik.mobibot + +import org.testng.annotations.BeforeSuite +import java.io.IOException +import java.net.InetAddress +import java.net.UnknownHostException +import java.nio.file.Files +import java.nio.file.Paths +import java.util.* + +/** + * Access to `local.properties`. + */ +open class LocalProperties { + @BeforeSuite(alwaysRun = true) + fun loadProperties() { + val localPath = Paths.get("local.properties") + if (Files.exists(localPath)) { + try { + Files.newInputStream(localPath).use { stream -> localProps.load(stream) } + } catch (ignore: IOException) { + // Do nothing + } + } + } + + companion object { + private val localProps = Properties() + + fun getHostName(): String { + val ciName = System.getenv("CI_NAME") + return ciName ?: try { + InetAddress.getLocalHost().hostName + } catch (ignore: UnknownHostException) { + "Unknown Host" + } + } + + 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/bin/test/net/thauvin/erik/mobibot/PinboardTest.kt b/bin/test/net/thauvin/erik/mobibot/PinboardTest.kt new file mode 100644 index 0000000..b527fc5 --- /dev/null +++ b/bin/test/net/thauvin/erik/mobibot/PinboardTest.kt @@ -0,0 +1,81 @@ +/* + * PinboardTest.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package net.thauvin.erik.mobibot + +import net.thauvin.erik.mobibot.Utils.encodeUrl +import net.thauvin.erik.mobibot.Utils.reader +import net.thauvin.erik.mobibot.entries.EntryLink +import org.testng.Assert.assertFalse +import org.testng.Assert.assertTrue +import kotlin.test.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/${(1000..5000).random()}" + 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), "addPin") + + entry.link = "https://www.example.com/${(5001..9999).random()}" + pinboard.updatePin(ircServer, url, entry) + assertTrue(validatePin(apiToken, url = entry.link, ircServer), "updatePin") + + entry.title = "Foo Title" + pinboard.updatePin(ircServer, entry.link, entry) + assertTrue(validatePin(apiToken, url = entry.link, entry.title), "updatePin(${entry.title}") + + pinboard.deletePin(entry) + assertFalse(validatePin(apiToken, url = entry.link), "deletePin") + } + + private fun validatePin(apiToken: String, url: String, vararg matches: String): Boolean { + val response = + URL("https://api.pinboard.in/v1/posts/get?auth_token=${apiToken}&tag=test&" + url.encodeUrl()).reader().body + + matches.forEach { + if (!response.contains(it)) { + return false + } + } + + return response.contains(url) + } +} diff --git a/bin/test/net/thauvin/erik/mobibot/UtilsTest.kt b/bin/test/net/thauvin/erik/mobibot/UtilsTest.kt new file mode 100644 index 0000000..7b7ba8c --- /dev/null +++ b/bin/test/net/thauvin/erik/mobibot/UtilsTest.kt @@ -0,0 +1,278 @@ +/* + * UtilsTest.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.thauvin.erik.mobibot + +import 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.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.helpCmdSyntax +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.reader +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.underline +import net.thauvin.erik.mobibot.Utils.unescapeXml +import net.thauvin.erik.mobibot.msg.Message.Companion.DEFAULT_COLOR +import org.pircbotx.Colors +import org.testng.annotations.BeforeClass +import kotlin.test.Test +import java.io.File +import java.io.IOException +import java.net.URL +import java.time.LocalDateTime +import java.util.* + +/** + * 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(), "bold(1)").isEqualTo(Colors.BOLD + "1" + Colors.BOLD) + assertThat(2L.bold(), "bold(2L)").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 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), "ascii.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("Hello Günter".encodeUrl()).isEqualTo("Hello%20G%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 testHelpCmdSyntax() { + val bot = "mobibot" + assertThat(helpCmdSyntax("%c $test %n $test", bot, false), "helpCmdSyntax(private)") + .isEqualTo("$bot: $test $bot $test") + assertThat(helpCmdSyntax("%c %n $test %c $test %n", bot, true), "helpCmdSyntax(public)") + .isEqualTo("/msg $bot $bot $test /msg $bot $test $bot") + } + + @Test + fun testHelpFormat() { + assertThat(helpFormat(test, isBold = true, isIndent = false), "helpFormat(bold)") + .isEqualTo("${Colors.BOLD}$test${Colors.BOLD}") + assertThat(helpFormat(test, isBold = false, isIndent = true), "helpFormat(indent)") + .isEqualTo(test.prependIndent()) + assertThat(helpFormat(test, isBold = true, isIndent = true), "helpFormat(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(), "lastOrEmpty(1,2)").isEqualTo("2") + val one = listOf("1") + assertThat(one.lastOrEmpty(), "lastOrEmpty(1)").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 testUnderline() { + assertThat(ascii.underline()).isEqualTo(ascii.colorize(Colors.UNDERLINE)) + } + + @Test + fun testUnescapeXml() { + assertThat("<a name="test & ''">".unescapeXml()).isEqualTo( + "" + ) + } + + @Test + @Throws(IOException::class) + fun testUrlReader() { + val reader = URL("https://postman-echo.com/status/200").reader() + assertThat(reader.body).isEqualTo("{\n \"status\": 200\n}") + assertThat(reader.responseCode).isEqualTo(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/bin/test/net/thauvin/erik/mobibot/commands/InfoTest.kt b/bin/test/net/thauvin/erik/mobibot/commands/InfoTest.kt new file mode 100644 index 0000000..99ba951 --- /dev/null +++ b/bin/test/net/thauvin/erik/mobibot/commands/InfoTest.kt @@ -0,0 +1,58 @@ +/* + * InfoTest.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package net.thauvin.erik.mobibot.commands + +import assertk.assertThat +import assertk.assertions.isEqualTo +import net.thauvin.erik.mobibot.commands.Info.Companion.toUptime +import kotlin.test.Test + +class InfoTest { + @Test + fun testToUptime() { + assertThat( + 547800300076L.toUptime(), + "upTime(full)" + ).isEqualTo("17 years 4 months 2 weeks 1 day 6 hours 45 minutes") + assertThat(24300000L.toUptime(), "upTime(hours minutes)").isEqualTo("6 hours 45 minutes") + assertThat(110700000L.toUptime(), "upTime(days hours minutes)").isEqualTo("1 day 6 hours 45 minutes") + assertThat( + 1320300000L.toUptime(), + "upTime(weeks days hours minutes)" + ).isEqualTo("2 weeks 1 day 6 hours 45 minutes") + assertThat(2700000L.toUptime(), "upTime(45 minutes)").isEqualTo("45 minutes") + assertThat(60000L.toUptime(), "upTime(1 minute)").isEqualTo("1 minute") + assertThat(59000L.toUptime(), "upTime(59 seconds)").isEqualTo("59 seconds") + assertThat(0L.toUptime(), "upTime(0 second)").isEqualTo("0 second") + + } +} diff --git a/bin/test/net/thauvin/erik/mobibot/commands/RecapTest.kt b/bin/test/net/thauvin/erik/mobibot/commands/RecapTest.kt new file mode 100644 index 0000000..b6fbbff --- /dev/null +++ b/bin/test/net/thauvin/erik/mobibot/commands/RecapTest.kt @@ -0,0 +1,60 @@ +/* + * RecapTest.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package net.thauvin.erik.mobibot.commands + +import assertk.all +import assertk.assertThat +import assertk.assertions.isEqualTo +import assertk.assertions.matches +import assertk.assertions.prop +import assertk.assertions.size +import kotlin.test.Test + +class RecapTest { + @Test + fun storeRecapTest() { + for (i in 1..20) { + Recap.storeRecap("sender$i", "test $i", false) + } + assertThat(Recap.recaps, "Recap.recaps").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/bin/test/net/thauvin/erik/mobibot/commands/links/LinksManagerTest.kt b/bin/test/net/thauvin/erik/mobibot/commands/links/LinksManagerTest.kt new file mode 100644 index 0000000..fa2bb70 --- /dev/null +++ b/bin/test/net/thauvin/erik/mobibot/commands/links/LinksManagerTest.kt @@ -0,0 +1,77 @@ +/* + * LinksManagerTest.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package net.thauvin.erik.mobibot.commands.links + +import 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 kotlin.test.Test + +class LinksManagerTest { + private val linksManager = LinksManager() + + @Test + fun fetchTitle() { + assertThat(linksManager.fetchTitle("https://erik.thauvin.net/"), "fetchTitle(Erik)").contains("Erik's Weblog") + assertThat( + linksManager.fetchTitle("https://www.google.com/foo"), + "fetchTitle(Foo)" + ).isEqualTo(Constants.NO_TITLE) + } + + @Test + fun testMatches() { + assertThat(linksManager.matches("https://www.example.com/"), "matches(url)").isTrue() + assertThat(linksManager.matches("HTTP://erik.thauvin.net/blog/ Erik's Weblog"), "matches(HTTP)").isTrue() + } + + @Test + fun matchTagKeywordsTest() { + linksManager.setProperty(LinksManager.KEYWORDS_PROP, "key1 key2,key3") + val tags = mutableListOf() + + linksManager.matchTagKeywords("Test title with key2", tags) + assertThat(tags, "tags").contains("key2") + tags.clear() + + linksManager.matchTagKeywords("Test key3 title with key1", tags) + assertThat(tags, "tags(key1, key3)").all { + contains("key1") + contains("key3") + size().isEqualTo(2) + } + } +} diff --git a/bin/test/net/thauvin/erik/mobibot/commands/links/ViewTest.kt b/bin/test/net/thauvin/erik/mobibot/commands/links/ViewTest.kt new file mode 100644 index 0000000..e315891 --- /dev/null +++ b/bin/test/net/thauvin/erik/mobibot/commands/links/ViewTest.kt @@ -0,0 +1,111 @@ +/* + * ViewTest.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package net.thauvin.erik.mobibot.commands.links + +import assertk.all +import assertk.assertThat +import assertk.assertions.isEqualTo +import assertk.assertions.prop +import net.thauvin.erik.mobibot.entries.EntryLink +import kotlin.test.Test + +class ViewTest { + @Test + fun testParseArgs() { + val view = View() + + for (i in 1..10) { + LinksManager.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(LinksManager.entries.links.size - View.MAX_ENTRIES) + prop(Pair::second).isEqualTo("") + } + + LinksManager.entries.links.clear() + + assertThat(view.parseArgs("4"), "parseArgs(4)").all { + prop(Pair::first).isEqualTo(0) + prop(Pair::second).isEqualTo("") + } + } +} diff --git a/bin/test/net/thauvin/erik/mobibot/commands/seen/SeenTest.kt b/bin/test/net/thauvin/erik/mobibot/commands/seen/SeenTest.kt new file mode 100644 index 0000000..37b884b --- /dev/null +++ b/bin/test/net/thauvin/erik/mobibot/commands/seen/SeenTest.kt @@ -0,0 +1,85 @@ +/* + * SeenTest.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package net.thauvin.erik.mobibot.commands.seen + +import assertk.all +import assertk.assertThat +import assertk.assertions.* +import org.testng.annotations.AfterClass +import org.testng.annotations.BeforeClass +import kotlin.test.Test +import kotlin.io.path.deleteIfExists +import kotlin.io.path.fileSize + +class SeenTest { + private val tmpFile = kotlin.io.path.createTempFile(suffix = ".ser") + private val seen = Seen(tmpFile.toAbsolutePath().toString()) + private val nick = "ErikT" + + @BeforeClass + fun saveTest() { + seen.add("ErikT") + assertThat(tmpFile.fileSize(), "tmpFile.size").isGreaterThan(0) + } + + @AfterClass(alwaysRun = true) + fun afterClass() { + tmpFile.deleteIfExists() + } + + @Test(priority = 1, groups = ["commands"]) + fun loadTest() { + seen.clear() + assertThat(seen::seenNicks).isEmpty() + seen.load() + assertThat(seen::seenNicks).key(nick).isNotNull() + } + + @Test + fun addTest() { + val last = seen.seenNicks[nick]?.lastSeen + seen.add(nick.lowercase()) + assertThat(seen).all { + prop(Seen::seenNicks).size().isEqualTo(1) + prop(Seen::seenNicks).key(nick).isNotNull().prop(SeenNick::lastSeen).isNotEqualTo(last) + prop(Seen::seenNicks).key(nick).isNotNull().prop(SeenNick::nick).isNotNull().isEqualTo(nick.lowercase()) + } + } + + @Test(priority = 10, groups = ["commands"]) + fun clearTest() { + seen.clear() + seen.save() + seen.load() + assertThat(seen::seenNicks).size().isEqualTo(0) + } +} diff --git a/bin/test/net/thauvin/erik/mobibot/commands/tell/TellMessageTest.kt b/bin/test/net/thauvin/erik/mobibot/commands/tell/TellMessageTest.kt new file mode 100644 index 0000000..cc09237 --- /dev/null +++ b/bin/test/net/thauvin/erik/mobibot/commands/tell/TellMessageTest.kt @@ -0,0 +1,72 @@ +/* + * TellMessageTest.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.thauvin.erik.mobibot.commands.tell + +import assertk.all +import assertk.assertThat +import assertk.assertions.isEqualTo +import assertk.assertions.isFalse +import assertk.assertions.isTrue +import assertk.assertions.prop +import kotlin.test.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), "isValidDate()").isTrue() + assertThat(tellMessage.isMatch(sender), "isMatch(sender)").isTrue() + assertThat(tellMessage.isMatch(recipient), "isMatch(recipient)").isTrue() + assertThat(tellMessage.isMatch("foo"), "isMatch(foo)").isFalse() + tellMessage.isReceived = false + assertThat(tellMessage.receptionDate, "receptionDate").isEqualTo(LocalDateTime.MIN) + tellMessage.isReceived = true + assertThat(isValidDate(tellMessage.receptionDate), "isValidDate(creationDate)").isTrue() + } +} diff --git a/bin/test/net/thauvin/erik/mobibot/commands/tell/TellMessagesMgrTest.kt b/bin/test/net/thauvin/erik/mobibot/commands/tell/TellMessagesMgrTest.kt new file mode 100644 index 0000000..5acba78 --- /dev/null +++ b/bin/test/net/thauvin/erik/mobibot/commands/tell/TellMessagesMgrTest.kt @@ -0,0 +1,87 @@ +/* + * TellMessagesMgrTest.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package net.thauvin.erik.mobibot.commands.tell + +import assertk.all +import assertk.assertThat +import assertk.assertions.* +import org.testng.annotations.AfterClass +import org.testng.annotations.BeforeClass +import kotlin.test.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() { + TellManager.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(TellManager.clean(testMessages, maxDays + 2), "clean(maxDays=${maxDays + 2})").isFalse() + assertThat(TellManager.clean(testMessages, maxDays), "clean(maxDays=$maxDays)").isTrue() + assertThat(testMessages, "testMessages").size().isEqualTo(size - 1) + } + + @Test + fun loadTest() { + val messages = TellManager.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/bin/test/net/thauvin/erik/mobibot/entries/EntriesUtilsTest.kt b/bin/test/net/thauvin/erik/mobibot/entries/EntriesUtilsTest.kt new file mode 100644 index 0000000..0ad26b5 --- /dev/null +++ b/bin/test/net/thauvin/erik/mobibot/entries/EntriesUtilsTest.kt @@ -0,0 +1,91 @@ +/* + * EntriesUtilsTest.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package net.thauvin.erik.mobibot.entries + +import assertk.assertThat +import assertk.assertions.contains +import assertk.assertions.isEqualTo +import net.thauvin.erik.mobibot.Constants +import net.thauvin.erik.mobibot.entries.EntriesUtils.printComment +import net.thauvin.erik.mobibot.entries.EntriesUtils.printLink +import net.thauvin.erik.mobibot.entries.EntriesUtils.printTags +import net.thauvin.erik.mobibot.entries.EntriesUtils.toLinkLabel +import kotlin.test.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 printCommentTest() { + assertThat(printComment(0, 0, comment)).isEqualTo("${Constants.LINK_CMD}1.1: [nick] comment") + } + + @Test + fun printLinkTest() { + for (i in links.indices) { + assertThat( + printLink(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), "addComment()").isEqualTo(0) + assertThat(printLink(0, links.first(), isView = true), "printLink(isView=true)").contains("[+1]") + } + + @Test + fun printTagsTest() { + for (i in links.indices) { + assertThat( + printTags(i - 1, links[i]), "tag $i" + ).isEqualTo("L${i}T: tag1, tag2, tag3, tag4, tag5") + } + } + + @Test + fun toLinkLabelTest() { + assertThat(1.toLinkLabel()).isEqualTo("${Constants.LINK_CMD}2") + } +} diff --git a/bin/test/net/thauvin/erik/mobibot/entries/EntryLinkTest.kt b/bin/test/net/thauvin/erik/mobibot/entries/EntryLinkTest.kt new file mode 100644 index 0000000..f421099 --- /dev/null +++ b/bin/test/net/thauvin/erik/mobibot/entries/EntryLinkTest.kt @@ -0,0 +1,133 @@ +/* + * EntryLinkTest.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.thauvin.erik.mobibot.entries + +import assertk.all +import assertk.assertThat +import assertk.assertions.* +import com.rometools.rome.feed.synd.SyndCategory +import com.rometools.rome.feed.synd.SyndCategoryImpl +import kotlin.test.Test +import java.security.SecureRandom +import java.util.* + +/** + * 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, "comments").size().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, "comment[first]").all { + prop(EntryComment::nick).isEqualTo("somebody") + prop(EntryComment::comment).isEqualTo("something") + } + assertThat(entryLink.deleteComment(comment), "deleteComment").isTrue() + assertThat(entryLink.deleteComment(comment), "comment is already deleted").isFalse() + } + + @Test + fun testConstructor() { + val tags = listOf(SyndCategoryImpl().apply { name = "tag1" }, SyndCategoryImpl().apply { name = "tag2" }) + val link = EntryLink("link", "title", "nick", "channel", Date(), tags) + assertThat(link, "link").all { + prop(EntryLink::tags).size().isEqualTo(tags.size) + prop(EntryLink::tags).index(0).prop(SyndCategory::getName).isEqualTo("tag1") + } + } + + @Test + fun testMatches() { + assertThat(entryLink.matches("mobitopia"), "matches(mobitopia)").isTrue() + assertThat(entryLink.matches("skynx"), "match(nick)").isTrue() + assertThat(entryLink.matches("www.mobitopia.org"), "matches(url)").isTrue() + assertThat(entryLink.matches("foo"), "matches(foo)").isFalse() + assertThat(entryLink.matches(""), "matches(empty)").isFalse() + assertThat(entryLink.matches(null), "matches(null)").isFalse() + } + + + @Test + fun testTags() { + val tags: List = entryLink.tags + for ((i, tag) in tags.withIndex()) { + assertThat(tag.name, "tag.name($i)").isEqualTo("tag${i + 1}") + } + assertThat(entryLink::tags).size().isEqualTo(5) + entryLink.setTags("-tag5, tag4") + entryLink.setTags("+mobitopia") + entryLink.setTags("-mobitopia") + assertThat( + entryLink.formatTags(","), + "formatTags(',')" + ).isEqualTo("tag1,tag2,tag3,tag4,mobitopia") + entryLink.setTags("-tag4 tag5") + assertThat( + entryLink.formatTags(" ", ","), "formatTag(' ',',')" + ).isEqualTo(",tag1 tag2 tag3 mobitopia tag5") + val size = entryLink.tags.size + entryLink.setTags("") + assertThat(entryLink.tags, "setTags('')").size().isEqualTo(size) + entryLink.setTags(" ") + assertThat(entryLink.tags, "setTags(' ')").size().isEqualTo(size) + } +} diff --git a/bin/test/net/thauvin/erik/mobibot/entries/FeedMgrTest.kt b/bin/test/net/thauvin/erik/mobibot/entries/FeedMgrTest.kt new file mode 100644 index 0000000..1611009 --- /dev/null +++ b/bin/test/net/thauvin/erik/mobibot/entries/FeedMgrTest.kt @@ -0,0 +1,115 @@ +/* + * FeedMgrTest.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package net.thauvin.erik.mobibot.entries + +import assertk.all +import assertk.assertThat +import assertk.assertions.* +import net.thauvin.erik.mobibot.Utils.today +import org.testng.annotations.BeforeSuite +import kotlin.test.Test +import java.nio.file.Paths +import java.util.* +import kotlin.io.path.deleteIfExists +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(FeedsManager.loadFeed(entries), "loadFeed()").isEqualTo("2021-10-31") + + assertThat(entries.links, "entries.links").size().isEqualTo(2) + entries.links.forEachIndexed { i, entryLink -> + assertThat(entryLink, "entryLink[${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, "nick[first]").isEqualTo("ErikT") + assertThat(date, "date[first]").isEqualTo(Date(1635638400000L)) + assertThat(comments.first(), "comments[first]").all { + prop(EntryComment::comment).endsWith("comment 1.") + prop(EntryComment::nick).isEqualTo("ErikT") + } + assertThat(comments.last(), "comments[last]").all { + prop(EntryComment::comment).endsWith("comment 2.") + prop(EntryComment::nick).isEqualTo("Skynx") + } + } + + assertThat(entries.links, "links").index(1).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 + FeedsManager.saveFeed(entries, currentFile.name) + + assertThat(currentFile, "currentFile").exists() + assertThat(backlogFile, "backlogFile").exists() + + assertThat(currentFile.fileSize(), "currentFile == backlogFile").isEqualTo(backlogFile.fileSize()) + + // Load the test feed + entries.links.clear() + FeedsManager.loadFeed(entries, currentFile.name) + + entries.links.forEachIndexed { i, entryLink -> + assertThat(entryLink.title, "entryLink.title[${i + 1}]").isEqualTo("Example ${i + 1}") + } + + assertThat(currentFile.deleteIfExists(), "currentFile.deleteIfExists()").isTrue() + assertThat(backlogFile.deleteIfExists(), "backlogFile.deleteIfExists()").isTrue() + } +} diff --git a/bin/test/net/thauvin/erik/mobibot/modules/CalcTest.kt b/bin/test/net/thauvin/erik/mobibot/modules/CalcTest.kt new file mode 100644 index 0000000..3bd4c0f --- /dev/null +++ b/bin/test/net/thauvin/erik/mobibot/modules/CalcTest.kt @@ -0,0 +1,53 @@ +/* + * CalcTest.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.thauvin.erik.mobibot.modules + +import assertk.assertFailure +import assertk.assertThat +import assertk.assertions.isEqualTo +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 kotlin.test.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()}") + assertFailure { calculate("one + one") }.isInstanceOf(UnknownFunctionOrVariableException::class.java) + } +} diff --git a/bin/test/net/thauvin/erik/mobibot/modules/ChatGptTest.kt b/bin/test/net/thauvin/erik/mobibot/modules/ChatGptTest.kt new file mode 100644 index 0000000..085f228 --- /dev/null +++ b/bin/test/net/thauvin/erik/mobibot/modules/ChatGptTest.kt @@ -0,0 +1,62 @@ +/* + * ChatGptTest.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.thauvin.erik.mobibot.modules + +import assertk.assertFailure +import assertk.assertThat +import assertk.assertions.contains +import assertk.assertions.hasNoCause +import assertk.assertions.isInstanceOf +import net.thauvin.erik.mobibot.LocalProperties +import kotlin.test.Test + +class ChatGptTest : LocalProperties() { + @Test + fun testApiKey() { + assertFailure { ChatGpt.chat("1 gallon to liter", "", 0) } + .isInstanceOf(ModuleException::class.java) + .hasNoCause() + } + + @Test(groups = ["modules", "no-ci"]) + fun testChat() { + val apiKey = getProperty(ChatGpt.API_KEY_PROP) + assertThat( + ChatGpt.chat("how do I make an HTTP request in Javascript?", apiKey, 100) + ).contains("XMLHttpRequest") + assertThat( + ChatGpt.chat("how do I encode a URL in java?", apiKey, 60) + ).contains("URLEncoder") + + assertFailure { ChatGpt.chat("1 liter to gallon", apiKey, 0) } + .isInstanceOf(ModuleException::class.java) + } +} diff --git a/bin/test/net/thauvin/erik/mobibot/modules/CryptoPricesTest.kt b/bin/test/net/thauvin/erik/mobibot/modules/CryptoPricesTest.kt new file mode 100644 index 0000000..d0ecbc9 --- /dev/null +++ b/bin/test/net/thauvin/erik/mobibot/modules/CryptoPricesTest.kt @@ -0,0 +1,78 @@ +/* + * CryptoPricesTest.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.thauvin.erik.mobibot.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 net.thauvin.erik.mobibot.modules.CryptoPrices.Companion.getCurrencyName +import net.thauvin.erik.mobibot.modules.CryptoPrices.Companion.loadCurrencies +import org.testng.annotations.BeforeClass +import kotlin.test.Test + +/** + * The `CryptoPricesTest` class. + */ +class CryptoPricesTest { + @BeforeClass + @Throws(ModuleException::class) + fun before() { + loadCurrencies() + } + + @Test + @Throws(ModuleException::class) + fun testMarketPrice() { + var price = currentPrice(listOf("BTC")) + assertThat(price, "currentPrice(BTC)").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, "currentPrice(ETH, EUR)").all { + prop(CryptoPrice::base).isEqualTo("ETH") + prop(CryptoPrice::currency).isEqualTo("EUR") + prop(CryptoPrice::amount).transform { it.signum() }.isGreaterThan(0) + } + } + + @Test + fun testGetCurrencyName() { + assertThat(getCurrencyName("USD"), "USD").isEqualTo("United States Dollar") + assertThat(getCurrencyName("EUR"), "EUR").isEqualTo("Euro") + } +} diff --git a/bin/test/net/thauvin/erik/mobibot/modules/CurrencyConverterTest.kt b/bin/test/net/thauvin/erik/mobibot/modules/CurrencyConverterTest.kt new file mode 100644 index 0000000..8d8c997 --- /dev/null +++ b/bin/test/net/thauvin/erik/mobibot/modules/CurrencyConverterTest.kt @@ -0,0 +1,85 @@ +/* + * CurrencyConverterTest.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.thauvin.erik.mobibot.modules + +import assertk.all +import assertk.assertThat +import assertk.assertions.contains +import assertk.assertions.isInstanceOf +import assertk.assertions.matches +import assertk.assertions.prop +import net.thauvin.erik.mobibot.LocalProperties +import net.thauvin.erik.mobibot.modules.CurrencyConverter.Companion.convertCurrency +import net.thauvin.erik.mobibot.modules.CurrencyConverter.Companion.loadSymbols +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 kotlin.test.Test + + +/** + * The `CurrencyConvertTest` class. + */ +class CurrencyConverterTest : LocalProperties() { + + @BeforeClass + @Throws(ModuleException::class) + fun before() { + val apiKey = getProperty(CurrencyConverter.API_KEY_PROP) + loadSymbols(apiKey) + } + + @Test + fun testConvertCurrency() { + val apiKey = getProperty(CurrencyConverter.API_KEY_PROP) + assertThat( + convertCurrency(apiKey, "100 USD to EUR").msg, + "convertCurrency(100 USD to EUR)" + ).matches("100 United States Dollar = \\d{2,3}\\.\\d{2,3} Euro".toRegex()) + assertThat( + convertCurrency(apiKey, "1 USD to GBP").msg, + "convertCurrency(1 USD to BGP)" + ).matches("1 United States Dollar = 0\\.\\d{2,3} Pound Sterling".toRegex()) + assertThat( + convertCurrency(apiKey, "100,000.00 CAD to USD").msg, + "convertCurrency(100,000.00 GBP to USD)" + ).matches("100,000.00 Canadian Dollar = \\d+\\.\\d{2,3} United States Dollar".toRegex()) + assertThat(convertCurrency(apiKey, "100 USD to USD"), "convertCurrency(100 USD to USD)").all { + prop(Message::msg).contains("You're kidding, right?") + isInstanceOf(PublicMessage::class.java) + } + assertThat(convertCurrency(apiKey, "100 USD"), "convertCurrency(100 USD)").all { + prop(Message::msg).contains("Invalid query.") + isInstanceOf(ErrorMessage::class.java) + } + } +} diff --git a/bin/test/net/thauvin/erik/mobibot/modules/DiceTest.kt b/bin/test/net/thauvin/erik/mobibot/modules/DiceTest.kt new file mode 100644 index 0000000..8bafede --- /dev/null +++ b/bin/test/net/thauvin/erik/mobibot/modules/DiceTest.kt @@ -0,0 +1,53 @@ +/* + * DiceTest.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package net.thauvin.erik.mobibot.modules + + +import assertk.assertThat +import assertk.assertions.isEqualTo +import assertk.assertions.matches +import kotlin.test.Test + +class DiceTest { + @Test + fun testRoll() { + assertThat(Dice.roll(1, 1), "roll(1d1)").isEqualTo("\u00021\u0002") + assertThat(Dice.roll(2, 1), "roll(2d1)") + .isEqualTo("\u00021\u0002 + \u00021\u0002 = \u00022\u0002") + assertThat(Dice.roll(5, 1), "roll(5d1)") + .isEqualTo("\u00021\u0002 + \u00021\u0002 + \u00021\u0002 + \u00021\u0002 + \u00021\u0002 = \u00025\u0002") + assertThat(Dice.roll(2, 6), "roll(2d6)") + .matches("\u0002[1-6]\u0002 \\+ \u0002[1-6]\u0002 = \u0002[1-9][0-2]?\u0002".toRegex()) + assertThat(Dice.roll(3, 7), "roll(3d7)") + .matches("\u0002[1-7]\u0002 \\+ \u0002[1-7]\u0002 \\+ \u0002[1-7]\u0002 = \u0002\\d{1,2}\u0002".toRegex()) + } +} diff --git a/bin/test/net/thauvin/erik/mobibot/modules/GoogleSearchTest.kt b/bin/test/net/thauvin/erik/mobibot/modules/GoogleSearchTest.kt new file mode 100644 index 0000000..85c990c --- /dev/null +++ b/bin/test/net/thauvin/erik/mobibot/modules/GoogleSearchTest.kt @@ -0,0 +1,95 @@ +/* + * GoogleSearchTest.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.thauvin.erik.mobibot.modules + +import assertk.all +import assertk.assertFailure +import assertk.assertThat +import assertk.assertions.* +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 kotlin.test.Test + +/** + * The `GoogleSearchTest` class. + */ +class GoogleSearchTest : LocalProperties() { + @Test + fun testAPIKeys() { + assertThat( + searchGoogle("", "apikey", "cssKey").first(), + "searchGoogle(empty)" + ).isInstanceOf(ErrorMessage::class.java) + + assertFailure { searchGoogle("test", "", "apiKey") } + .isInstanceOf(ModuleException::class.java).hasNoCause() + + assertFailure { searchGoogle("test", "apiKey", "") } + .isInstanceOf(ModuleException::class.java).hasNoCause() + + assertFailure { searchGoogle("test", "apiKey", "cssKey") } + .isInstanceOf(ModuleException::class.java) + .hasMessage("API key not valid. Please pass a valid API key.") + } + + @Test\n@DisableOnCi + @Throws(ModuleException::class) + fun testSearchGoogle() { + val apiKey = getProperty(GoogleSearch.API_KEY_PROP) + val cseKey = getProperty(GoogleSearch.CSE_KEY_PROP) + + try { + var query = "mobibot" + var messages = searchGoogle(query, apiKey, cseKey) + assertThat(messages, "searchGoogle($query)").all { + isNotEmpty() + index(0).prop(Message::msg).contains(query, true) + } + + query = "adadflkjl" + messages = searchGoogle(query, apiKey, cseKey) + assertThat(messages, "searchGoogle($query)").index(0).all { + isInstanceOf(ErrorMessage::class.java) + prop(Message::msg).isEqualTo("No results found.") + } + } 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/bin/test/net/thauvin/erik/mobibot/modules/JokeTest.kt b/bin/test/net/thauvin/erik/mobibot/modules/JokeTest.kt new file mode 100644 index 0000000..0af8e75 --- /dev/null +++ b/bin/test/net/thauvin/erik/mobibot/modules/JokeTest.kt @@ -0,0 +1,57 @@ +/* + * JokeTest.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.thauvin.erik.mobibot.modules + +import assertk.all +import assertk.assertThat +import assertk.assertions.* +import net.thauvin.erik.mobibot.modules.Joke.Companion.randomJoke +import net.thauvin.erik.mobibot.msg.Message +import net.thauvin.erik.mobibot.msg.PublicMessage +import kotlin.test.Test + +/** + * The `JokeTest` class. + */ +class JokeTest { + @Test + @Throws(ModuleException::class) + fun testRandomJoke() { + val joke = randomJoke() + assertThat(joke, "randomJoke()").all { + size().isGreaterThan(0) + each { + it.isInstanceOf(PublicMessage::class.java) + it.prop(Message::msg).doesNotContain("\n") + } + } + } +} diff --git a/bin/test/net/thauvin/erik/mobibot/modules/LookupTest.kt b/bin/test/net/thauvin/erik/mobibot/modules/LookupTest.kt new file mode 100644 index 0000000..a3fc226 --- /dev/null +++ b/bin/test/net/thauvin/erik/mobibot/modules/LookupTest.kt @@ -0,0 +1,60 @@ +/* + * LookupTest.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.thauvin.erik.mobibot.modules + +import assertk.assertThat +import assertk.assertions.any +import assertk.assertions.contains +import net.thauvin.erik.mobibot.modules.Lookup.Companion.nslookup +import net.thauvin.erik.mobibot.modules.Lookup.Companion.whois +import kotlin.test.Test + +/** + * The `Lookup Test` class. + */ +class LookupTest { + @Test + @Throws(Exception::class) + fun testLookup() { + var result = nslookup("apple.com") + assertThat(result, "lookup(apple.com)").contains("17.253.144.10") + + result = nslookup("204.122.16.136") + assertThat(result, "lookup(204.122.16.136)").contains("nix3.thauvin.us") + } + + @Test + @Throws(Exception::class) + fun testWhois() { + val result = whois("17.178.96.59", Lookup.WHOIS_HOST) + assertThat(result, "whois(17.178.96.59").any { it.contains("Apple Inc.") } + } +} diff --git a/bin/test/net/thauvin/erik/mobibot/modules/MastodonTest.kt b/bin/test/net/thauvin/erik/mobibot/modules/MastodonTest.kt new file mode 100644 index 0000000..f4b5e99 --- /dev/null +++ b/bin/test/net/thauvin/erik/mobibot/modules/MastodonTest.kt @@ -0,0 +1,54 @@ +/* + * MastodonTest.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.thauvin.erik.mobibot.modules + +import assertk.assertThat +import assertk.assertions.contains +import net.thauvin.erik.mobibot.LocalProperties +import net.thauvin.erik.mobibot.modules.Mastodon.Companion.toot +import kotlin.test.Test + +class MastodonTest : LocalProperties() { + @Test + @Throws(ModuleException::class) + fun testToot() { + val msg = "Testing Mastodon API from ${getHostName()}" + assertThat( + toot( + getProperty(Mastodon.ACCESS_TOKEN_PROP), + getProperty(Mastodon.INSTANCE_PROP), + getProperty(Mastodon.HANDLE_PROP), + msg, + true + ) + ).contains(msg) + } +} diff --git a/bin/test/net/thauvin/erik/mobibot/modules/ModuleExceptionTest.kt b/bin/test/net/thauvin/erik/mobibot/modules/ModuleExceptionTest.kt new file mode 100644 index 0000000..1416eec --- /dev/null +++ b/bin/test/net/thauvin/erik/mobibot/modules/ModuleExceptionTest.kt @@ -0,0 +1,105 @@ +/* + * ModuleExceptionTest.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.thauvin.erik.mobibot.modules + +import assertk.all +import assertk.assertThat +import assertk.assertions.* +import net.thauvin.erik.mobibot.ExceptionSanitizer.sanitize +import org.testng.annotations.DataProvider +import kotlin.test.Test +import java.io.IOException +import java.lang.reflect.Method + +/** + * The `ModuleExceptionTest` class. + */ +class ModuleExceptionTest { + companion object { + const val DEBUG_MESSAGE = "debugMessage" + const val MESSAGE = "message" + } + + @DataProvider(name = "dp") + fun createData(@Suppress("UNUSED_PARAMETER") m: Method?): Array> { + return arrayOf( + arrayOf(ModuleException(DEBUG_MESSAGE, MESSAGE, IOException("URL http://foobar.com"))), + arrayOf(ModuleException(DEBUG_MESSAGE, MESSAGE, IOException("URL http://foobar.com?"))), + arrayOf(ModuleException(DEBUG_MESSAGE, MESSAGE)) + ) + } + + @Test(dataProvider = "dp") + fun testGetDebugMessage(e: ModuleException) { + assertThat(e::debugMessage).isEqualTo(DEBUG_MESSAGE) + } + + @Test(dataProvider = "dp") + fun testGetMessage(e: ModuleException) { + assertThat(e).hasMessage(MESSAGE) + } + + @Test + fun testSanitizeMessage() { + val apiKey = "1234567890" + var e = ModuleException(DEBUG_MESSAGE, MESSAGE, IOException("URL http://foo.com?apiKey=$apiKey&userID=me")) + assertThat( + e.sanitize(apiKey, "", "me").message, "ModuleException(debugMessage, message, IOException(url))" + ).isNotNull().all { + contains("xxxxxxxxxx", "userID=xx", "java.io.IOException") + doesNotContain(apiKey, "me") + } + + e = ModuleException(DEBUG_MESSAGE, MESSAGE, null) + assertThat(e.sanitize(apiKey), "ModuleException(debugMessage, message, null)").hasMessage(MESSAGE) + + e = ModuleException(DEBUG_MESSAGE, MESSAGE, IOException()) + assertThat(e.sanitize(apiKey), "ModuleException(debugMessage, message, IOException())").hasMessage(MESSAGE) + + e = ModuleException(DEBUG_MESSAGE, apiKey) + assertThat(e.sanitize(apiKey).message, "ModuleException(debugMessage, apiKey)").isNotNull() + .doesNotContain(apiKey) + + val msg: String? = null + e = ModuleException(DEBUG_MESSAGE, msg, IOException(msg)) + assertThat(e.sanitize(apiKey).message, "ModuleException(debugMessage, msg, IOException(msg))").isNull() + + e = ModuleException(DEBUG_MESSAGE, msg, IOException("foo is $apiKey")) + assertThat( + e.sanitize(" ", apiKey, "foo").message, + "ModuleException(debugMessage, msg, IOException(foo is $apiKey))" + ).isNotNull().all { + doesNotContain(apiKey) + endsWith("xxx is xxxxxxxxxx") + } + assertThat(e.sanitize(), "exception should be unchanged").isEqualTo(e) + } +} diff --git a/bin/test/net/thauvin/erik/mobibot/modules/PingTest.kt b/bin/test/net/thauvin/erik/mobibot/modules/PingTest.kt new file mode 100644 index 0000000..d1399e0 --- /dev/null +++ b/bin/test/net/thauvin/erik/mobibot/modules/PingTest.kt @@ -0,0 +1,54 @@ +/* + * PingTest.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.thauvin.erik.mobibot.modules + +import assertk.assertThat +import assertk.assertions.contains +import assertk.assertions.isNotEmpty +import net.thauvin.erik.mobibot.modules.Ping.Companion.randomPing +import kotlin.test.Test + +/** + * The `PingTest` class. + */ +class PingTest { + @Test + fun testPingsArray() { + assertThat(Ping.PINGS, "Ping.PINGS").isNotEmpty() + } + + @Test + fun testRandomPing() { + for (i in 0..9) { + assertThat(Ping.PINGS, "Ping.PINGS[$i]").contains(randomPing()) + } + } +} diff --git a/bin/test/net/thauvin/erik/mobibot/modules/RockPaperScissorsTest.kt b/bin/test/net/thauvin/erik/mobibot/modules/RockPaperScissorsTest.kt new file mode 100644 index 0000000..f836b0e --- /dev/null +++ b/bin/test/net/thauvin/erik/mobibot/modules/RockPaperScissorsTest.kt @@ -0,0 +1,50 @@ +/* + * RockPaperScissorsTest.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package net.thauvin.erik.mobibot.modules + +import assertk.assertThat +import assertk.assertions.isEqualTo +import net.thauvin.erik.mobibot.modules.RockPaperScissors.Companion.winLoseOrDraw +import kotlin.test.Test + +class RockPaperScissorsTest { + @Test + fun testWinLoseOrDraw() { + assertThat(winLoseOrDraw("scissors", "paper"), "scissors vs. paper").isEqualTo("win") + assertThat(winLoseOrDraw("paper", "rock"), "paper vs. rock").isEqualTo("win") + assertThat(winLoseOrDraw("rock", "scissors"), "rock vs. scissors").isEqualTo("win") + assertThat(winLoseOrDraw("paper", "scissors"), "paper vs. scissors").isEqualTo("lose") + assertThat(winLoseOrDraw("rock", "paper"), "rock vs. paper").isEqualTo("lose") + assertThat(winLoseOrDraw("scissors", "rock"), "scissors vs. rock").isEqualTo("lose") + assertThat(winLoseOrDraw("scissors", "scissors"), "scissors vs. scissors").isEqualTo("draw") + } +} diff --git a/bin/test/net/thauvin/erik/mobibot/modules/StockQuoteTest.kt b/bin/test/net/thauvin/erik/mobibot/modules/StockQuoteTest.kt new file mode 100644 index 0000000..d96812b --- /dev/null +++ b/bin/test/net/thauvin/erik/mobibot/modules/StockQuoteTest.kt @@ -0,0 +1,85 @@ +/* + * StockQuoteTest.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.thauvin.erik.mobibot.modules + +import assertk.all +import assertk.assertFailure +import assertk.assertThat +import assertk.assertions.* +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 kotlin.test.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.API_KEY_PROP) + try { + var symbol = "apple inc" + val messages = getQuote(symbol, apiKey) + assertThat(messages, "response not empty").isNotEmpty() + assertThat(messages, "getQuote($symbol)").index(0).prop(Message::msg).matches("Symbol: AAPL .*".toRegex()) + assertThat(messages, "getQuote($symbol)").index(1).prop(Message::msg).matches(buildMatch("Price").toRegex()) + assertThat(messages, "getQuote($symbol)").index(2).prop(Message::msg) + .matches(buildMatch("Previous").toRegex()) + assertThat(messages, "getQuote($symbol)").index(3).prop(Message::msg).matches(buildMatch("Open").toRegex()) + + symbol = "blahfoo" + assertThat(getQuote(symbol, apiKey).first(), "getQuote($symbol)").all { + isInstanceOf(ErrorMessage::class.java) + prop(Message::msg).isEqualTo(StockQuote.INVALID_SYMBOL) + } + assertThat(getQuote("", "apikey").first(), "getQuote(empty)").all { + isInstanceOf(ErrorMessage::class.java) + prop(Message::msg).isEqualTo(StockQuote.INVALID_SYMBOL) + } + assertFailure { getQuote("test", "") }.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/bin/test/net/thauvin/erik/mobibot/modules/Weather2Test.kt b/bin/test/net/thauvin/erik/mobibot/modules/Weather2Test.kt new file mode 100644 index 0000000..66f4d22 --- /dev/null +++ b/bin/test/net/thauvin/erik/mobibot/modules/Weather2Test.kt @@ -0,0 +1,117 @@ +/* + * Weather2Test.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.thauvin.erik.mobibot.modules + +import assertk.all +import assertk.assertFailure +import assertk.assertThat +import assertk.assertions.* +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.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 net.thauvin.erik.mobibot.msg.Message +import kotlin.test.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"), "foo is not a valid country").isEqualTo(OWM.Country.UNITED_STATES) + assertThat(getCountry("fr"), "country should France").isEqualTo(OWM.Country.FRANCE) + + val country = OWM.Country.entries.toTypedArray() + 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 query = "98204" + var messages = getWeather(query, getProperty(API_KEY_PROP)) + assertThat(messages, "getWeather($query)").index(0).prop(Message::msg).all { + contains("Everett, United States") + contains("US") + } + assertThat(messages, "getWeather($query)").index(messages.size - 1).prop(Message::msg).endsWith("98204%2CUS") + + query = "San Francisco" + messages = getWeather(query, getProperty(API_KEY_PROP)) + assertThat(messages, "getWeather($query)").index(0).prop(Message::msg).all { + contains("San Francisco") + contains("US") + } + assertThat(messages, "getWeather($query)").index(messages.size - 1).prop(Message::msg).endsWith("5391959") + + query = "London, GB" + messages = getWeather(query, getProperty(API_KEY_PROP)) + assertThat(messages, "getWeather($query)").index(0).prop(Message::msg).all { + contains("London, United Kingdom") + contains("GB") + } + assertThat(messages, "getWeather($query)").index(messages.size - 1).prop(Message::msg).endsWith("2643743") + + try { + query = "Foo, US" + getWeather(query, getProperty(API_KEY_PROP)) + } catch (e: ModuleException) { + assertThat(e.cause, "getWeather($query)").isNotNull().isInstanceOf(APIException::class.java) + } + + query = "test" + assertFailure { getWeather(query, "") }.isInstanceOf(ModuleException::class.java).hasNoCause() + assertFailure { getWeather(query, null) }.isInstanceOf(ModuleException::class.java).hasNoCause() + + messages = getWeather("", "apikey") + assertThat(messages, "getWeather(empty)").index(0).prop(Message::isError).isTrue() + } +} diff --git a/bin/test/net/thauvin/erik/mobibot/modules/WolframAlphaTest.kt b/bin/test/net/thauvin/erik/mobibot/modules/WolframAlphaTest.kt new file mode 100644 index 0000000..855522c --- /dev/null +++ b/bin/test/net/thauvin/erik/mobibot/modules/WolframAlphaTest.kt @@ -0,0 +1,77 @@ +/* + * WolframAlphaTest.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package net.thauvin.erik.mobibot.modules + +import assertk.assertFailure +import assertk.assertThat +import assertk.assertions.contains +import assertk.assertions.hasMessage +import assertk.assertions.isInstanceOf +import net.thauvin.erik.mobibot.ExceptionSanitizer.sanitize +import net.thauvin.erik.mobibot.LocalProperties +import net.thauvin.erik.mobibot.modules.WolframAlpha.Companion.queryWolfram +import kotlin.test.Test + +class WolframAlphaTest : LocalProperties() { + @Test + fun testAppId() { + assertFailure { queryWolfram("1 gallon to liter", appId = "DEMO") } + .isInstanceOf(ModuleException::class.java) + .hasMessage("Error 1: Invalid appid") + + assertFailure { queryWolfram("1 gallon to liter", appId = "") } + .isInstanceOf(ModuleException::class.java) + } + + @Test(groups = ["modules", "no-ci"]) + @Throws(ModuleException::class) + fun queryWolframTest() { + val apiKey = getProperty(WolframAlpha.APPID_KEY_PROP) + try { + var query = "SFO to SEA" + assertThat(queryWolfram(query, appId = apiKey), "queryWolfram($query)").contains("miles") + + query = "SFO to LAX" + assertThat( + queryWolfram(query, WolframAlpha.METRIC, apiKey), + "queryWolfram($query)" + ).contains("kilometers") + } catch (e: ModuleException) { + // Avoid displaying api key in CI logs + if ("true" == System.getenv("CI")) { + throw e.sanitize(apiKey) + } else { + throw e + } + } + } +} diff --git a/bin/test/net/thauvin/erik/mobibot/modules/WordTimeTest.kt b/bin/test/net/thauvin/erik/mobibot/modules/WordTimeTest.kt new file mode 100644 index 0000000..7931f33 --- /dev/null +++ b/bin/test/net/thauvin/erik/mobibot/modules/WordTimeTest.kt @@ -0,0 +1,70 @@ +/* + * WordTimeTest.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.thauvin.erik.mobibot.modules + +import assertk.assertThat +import assertk.assertions.endsWith +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 kotlin.test.Test +import java.time.ZoneId + +/** + * The `WordTimeTest` class. + */ +class WordTimeTest { + @Test + fun testTime() { + assertThat(time(), "time()").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(""), "time()").endsWith("Los Angeles".bold()) + assertThat(time("PST"), "time(PST)").endsWith("Los Angeles".bold()) + assertThat(time("GB"), "time(GB)").endsWith("London".bold()) + assertThat(time("FR"), "time(FR)").endsWith("Paris".bold()) + assertThat(time("BLAH"), "time(BLAH)").startsWith("Unsupported") + assertThat(time("BEAT"), "time($BEATS_KEYWORD)").matches("[\\w ]+ .?@\\d{3}+.? .beats".toRegex()) + } + + @Test + fun testZones() { + COUNTRIES_MAP.filter { it.value != BEATS_KEYWORD }.forEach { + assertThat(ZoneId.of(it.value)) + } + } +} diff --git a/bin/test/net/thauvin/erik/mobibot/msg/MessageTest.kt b/bin/test/net/thauvin/erik/mobibot/msg/MessageTest.kt new file mode 100644 index 0000000..32a0495 --- /dev/null +++ b/bin/test/net/thauvin/erik/mobibot/msg/MessageTest.kt @@ -0,0 +1,109 @@ +/* + * MessageTest.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package net.thauvin.erik.mobibot.msg + +import assertk.all +import assertk.assertThat +import assertk.assertions.isFalse +import assertk.assertions.isTrue +import assertk.assertions.prop +import kotlin.test.Test + +class MessageTest { + @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 testIsError() { + val msg = Message("foo") + msg.isError = true + assertThat(msg).all { + prop(Message::isError).isTrue() + prop(Message::isNotice).isTrue() + prop(Message::isPrivate).isFalse() + } + msg.isError = false + assertThat(msg).all { + prop(Message::isError).isFalse() + 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/bitbucket-pipelines.yml b/bitbucket-pipelines.yml index 623b3c3..7c85194 100644 --- a/bitbucket-pipelines.yml +++ b/bitbucket-pipelines.yml @@ -1,9 +1,10 @@ -image: maven:3-eclipse-temurin-17 +image: openjdk:17 pipelines: default: - step: - caches: - - gradle + name: Test with bld script: - - bash ./gradlew check + - ./bld download + - ./bld compile + - ./bld test diff --git a/bld b/bld new file mode 100755 index 0000000..77721d6 --- /dev/null +++ b/bld @@ -0,0 +1,2 @@ +#!/usr/bin/env sh +java -jar "$(dirname "$0")/lib/bld/bld-wrapper.jar" "$0" --build net.thauvin.erik.MobibotBuild "$@" \ No newline at end of file diff --git a/bld.bat b/bld.bat new file mode 100644 index 0000000..12ffa36 --- /dev/null +++ b/bld.bat @@ -0,0 +1,4 @@ +@echo off +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +java -jar "%DIRNAME%/lib/bld/bld-wrapper.jar" "%0" --build net.thauvin.erik.MobibotBuild %* \ No newline at end of file diff --git a/build.gradle b/build.gradle deleted file mode 100644 index 6507e20..0000000 --- a/build.gradle +++ /dev/null @@ -1,247 +0,0 @@ -import java.time.LocalDateTime -import java.time.format.DateTimeFormatter -import io.gitlab.arturbosch.detekt.Detekt -import io.gitlab.arturbosch.detekt.DetektCreateBaselineTask - -plugins { - id 'application' - id 'com.github.ben-manes.versions' version '0.49.0' - id 'idea' - id 'io.gitlab.arturbosch.detekt' version '1.23.3' - id 'java' - id 'net.thauvin.erik.gradle.semver' version '1.0.4' - id 'org.jetbrains.kotlin.jvm' version '1.9.20' - id 'org.jetbrains.kotlin.kapt' version '1.9.20' - id 'org.jetbrains.kotlinx.kover' version '0.7.4' - id 'org.sonarqube' version '4.4.1.3373' - id 'pmd' -} - -defaultTasks 'deploy' - -final def packageName = 'net.thauvin.erik.mobibot' -final def deployDir = 'deploy' -final def semverProcessor = "net.thauvin.erik:semver:1.2.1" - -final def isCI = (System.getenv('CI') != null) - -def isNonStable = { String version -> - def stableKeyword = ['RELEASE', 'FINAL', 'GA', 'JRE'].any { it -> version.toUpperCase().contains(it) } - def regex = /^[0-9,.v-]+(-r)?$/ - return !stableKeyword && !(version ==~ regex) -} - -mainClassName = packageName + '.Mobibot' - -ext.versions = [ - log4j: '2.21.1', - pmd : '6.55.0', -] - -repositories { - mavenLocal() - mavenCentral() - maven { url 'https://jitpack.io' } - maven { url 'https://oss.sonatype.org/content/repositories/snapshots' } -} - -dependencies { - kapt(semverProcessor) - compileOnly(semverProcessor) - - // PircBotX - implementation 'com.github.pircbotx:pircbotx:2.3.1' - // implementation fileTree(dir: 'lib', include: '*.jar') - - // Commons (mostly for PircBotX) - implementation 'org.apache.commons:commons-lang3:3.13.0' - implementation 'org.apache.commons:commons-text:1.11.0' - implementation 'commons-codec:commons-codec:1.16.0' - implementation 'commons-net:commons-net:3.10.0' - - // Google - implementation 'com.google.code.gson:gson:2.10.1' - implementation 'com.google.guava:guava:32.1.3-jre' - - // Kotlin - implementation platform('org.jetbrains.kotlin:kotlin-bom') - implementation 'org.jetbrains.kotlin:kotlin-stdlib' - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3' - implementation 'org.jetbrains.kotlinx:kotlinx-cli:0.3.6' - - // Logging - implementation 'org.slf4j:slf4j-api:2.0.9' - implementation "org.apache.logging.log4j:log4j-api:$versions.log4j" - implementation "org.apache.logging.log4j:log4j-core:$versions.log4j" - implementation "org.apache.logging.log4j:log4j-slf4j2-impl:$versions.log4j" - - implementation 'com.rometools:rome:2.1.0' - implementation 'com.squareup.okhttp3:okhttp:4.12.0' - implementation 'net.aksingh:owm-japis:2.5.3.0' - implementation 'net.objecthunter:exp4j:0.4.8' - implementation 'org.json:json:20231013' - implementation 'org.jsoup:jsoup:1.16.2' - - // Thauvin - implementation 'net.thauvin.erik:cryptoprice:1.0.1' - implementation 'net.thauvin.erik:jokeapi:0.9.0' - implementation 'net.thauvin.erik:pinboard-poster:1.1.0' - implementation 'net.thauvin.erik.urlencoder:urlencoder-lib:1.4.0' - - testImplementation 'com.willowtreeapps.assertk:assertk-jvm:0.27.0' -// testImplementation 'org.mockito.kotlin:mockito-kotlin:4.0.0' -// testImplementation "org.mockito:mockito-core:4.0.0" - testImplementation 'org.testng:testng:7.8.0' -} - -test { - useTestNG() { - // excludeGroups.add('twitter') - if (isCI) { - excludeGroups.add('no-ci') - } - if (!excludeGroups.isEmpty()) { - println "Excluded test groups: ${excludeGroups}" - } - } -} - -tasks.withType(Test).configureEach { - testLogging { - exceptionFormat = 'full' - events('skipped', 'failed') - } -} - -java { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 -} - -kotlin { - jvmToolchain { - languageVersion.set(JavaLanguageVersion.of(17)) - } -} - -kapt { - includeCompileClasspath = false - arguments { - arg('semver.project.dir', projectDir) - } -} - -tasks.withType(JavaCompile).configureEach { - options.encoding = 'UTF-8' -} - - -compileJava { - dependsOn 'incrementBuildMeta' - options.compilerArgs += ['-Xlint:unchecked', '-Xlint:deprecation'] -} - -tasks.named("dependencyUpdates").configure { - rejectVersionIf { - isNonStable(it.candidate.version) - } -} - -pmd { - toolVersion = versions.pmd - ignoreFailures = true - ruleSets = [] - ruleSetFiles = files("${projectDir}/config/pmd.xml") - consoleOutput = true -} - -detekt { - //toolVersion = "main-SNAPSHOT" - baseline = file("${projectDir}/config/detekt/baseline.xml") -} - -tasks.withType(Detekt).configureEach { - jvmTarget = java.targetCompatibility.toString() -} - -tasks.withType(DetektCreateBaselineTask).configureEach { - jvmTarget = java.targetCompatibility.toString() -} - -jar { - manifest.attributes('Main-Class': mainClassName, - 'Class-Path': '. ./lib/' + configurations.runtimeClasspath.collect { it.getName() }.join(' ./lib/')) - archiveVersion.set("") - exclude('log4j2.xml') -} - -clean { - doFirst { - project.delete(fileTree(deployDir)) - } -} - -run { - args('-h') -} - -incrementBuildMeta { - doFirst { - if (isCI) { - println 'No increment with CI.' - } else { - buildMeta = DateTimeFormatter.ofPattern("yyyyMMddHHmmss").format(LocalDateTime.now()) - } - } -} - -koverReport { - defaults { - xml { - onCheck = true - } - html { - onCheck = true - } - } -} - -sonarqube { - properties { - property('sonar.organization', 'ethauvin-github') - property('sonar.projectKey', 'ethauvin_mobibot') - property('sonar.host.url', 'https://sonarcloud.io') - property('sonar.coverage.jacoco.xmlReportPaths', "${layout.buildDirectory.get()}/reports/kover/report.xml") - } -} - -tasks.register('copyToDeploy', Copy) { - from('properties', jar) - into deployDir -} - -tasks.register('copyToDeployLib', Copy) { - from(configurations.runtimeClasspath) { - exclude 'annotations-*.jar' - } - into(deployDir + '/lib') -} - -tasks.register('deploy') { - description = "Copies all needed files to the ${deployDir} directory." - group = 'Publishing' - dependsOn(assemble, jar) - outputs.dir deployDir - inputs.files(copyToDeploy, copyToDeployLib) - doLast { - file(deployDir + '/logs').mkdir() - } - mustRunAfter(clean) -} - -tasks.register('release') { - group = 'Publishing' - description = 'Releases new version.' - dependsOn(clean, check, deploy) - mustRunAfter clean -} diff --git a/deploy.sh b/deploy.sh index ce3fde5..58dd32d 100755 --- a/deploy.sh +++ b/deploy.sh @@ -1,6 +1,6 @@ #!/bin/bash -./gradlew release +./bld jar deploy [ $? -eq 0 ] && sftp nix3.thauvin.us <nW$ZR+`W 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鯊%1Et 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⁢ zv5o9#E7!G0XmdzfsS{efPNi`1b44~SZ4Z8fuX!I}#8g+(wxzQwUT#Xb2(tbY1+EUhGKoT@KEU9Ktl>_0 z%bjDJg;#*gtJZv!-Zs`?^}v5eKmnbjqlvnSzE@_SP|LG_PJ6CYU+6zY6>92%E+ z=j@TZf-iW4(%U{lnYxQA;7Q!b;^brF8n0D>)`q5>|WDDXLrqYU_tKN2>=#@~OE7grMnNh?UOz-O~6 z6%rHy{#h9K0AT+lDC7q4{hw^|q6*Ry;;L%Q@)Ga}$60_q%D)rv(CtS$CQbpq9|y1e zRSrN4;$Jyl{m5bZw`$8TGvb}(LpY{-cQ)fcyJv7l3S52TLXVDsphtv&aPuDk1OzCA z4A^QtC(!11`IsNx_HnSy?>EKpHJWT^wmS~hc^p^zIIh@9f6U@I2 zC=Mve{j2^)mS#U$e{@Q?SO6%LDsXz@SY+=cK_QMmXBIU)j!$ajc-zLx3V60EXJ!qC zi<%2x8Q24YN+&8U@CIlN zrZkcT9yh%LrlGS9`G)KdP(@9Eo-AQz@8GEFWcb7U=a0H^ZVbLmz{+&M7W(nXJ4sN8 zJLR7eeK(K8`2-}j(T7JsO`L!+CvbueT%izanm-^A1Dn{`1Nw`9P?cq;7no+XfC`K(GO9?O^5zNIt4M+M8LM0=7Gz8UA@Z0N+lg+cX)NfazRu z5D)~HA^(u%w^cz+@2@_#S|u>GpB+j4KzQ^&Wcl9f z&hG#bCA(Yk0D&t&aJE^xME^&E-&xGHhXn%}psEIj641H+Nl-}boj;)Zt*t(4wZ5DN z@GXF$bL=&pBq-#vkTkh>7hl%K5|3 z{`Vn9b$iR-SoGENp}bn4;fR3>9sA%X2@1L3aE9yTra;Wb#_`xWwLSLdfu+PAu+o3| zGVnpzPr=ch{uuoHjtw7+_!L_2;knQ!DuDl0R`|%jr+}jFzXtrHIKc323?JO{l&;VF z*L1+}JU7%QJOg|5|Tc|D8fN zJORAg=_vsy{ak|o);@)Yh8Lkcg@$FG3k@ep36BRa^>~UmnRPziS>Z=`Jb2x*Q#`%A zU*i3&Vg?TluO@X0O;r2Jl6LKLUOVhSqg1*qOt^|8*c7 zo(298@+r$k_wQNGHv{|$tW(T8L+4_`FQ{kEW5Jgg{yf7ey4ss_(SNKfz(N9lx&a;< je(UuV8hP?p&}TPdm1I$XmG#(RzlD&B2izSj9sl%y5~4qc diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 3fa8f86..0000000 --- a/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,7 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip -networkTimeout=10000 -validateDistributionUrl=true -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew deleted file mode 100755 index 1aa94a4..0000000 --- a/gradlew +++ /dev/null @@ -1,249 +0,0 @@ -#!/bin/sh - -# -# Copyright © 2015-2021 the original authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -############################################################################## -# -# Gradle start up script for POSIX generated by Gradle. -# -# Important for running: -# -# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is -# noncompliant, but you have some other compliant shell such as ksh or -# bash, then to run this script, type that shell name before the whole -# command line, like: -# -# ksh Gradle -# -# Busybox and similar reduced shells will NOT work, because this script -# requires all of these POSIX shell features: -# * functions; -# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», -# «${var#prefix}», «${var%suffix}», and «$( cmd )»; -# * compound commands having a testable exit status, especially «case»; -# * various built-in commands including «command», «set», and «ulimit». -# -# Important for patching: -# -# (2) This script targets any POSIX shell, so it avoids extensions provided -# by Bash, Ksh, etc; in particular arrays are avoided. -# -# The "traditional" practice of packing multiple parameters into a -# space-separated string is a well documented source of bugs and security -# problems, so this is (mostly) avoided, by progressively accumulating -# options in "$@", and eventually passing that to Java. -# -# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, -# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; -# see the in-line comments for details. -# -# There are tweaks for specific operating systems such as AIX, CygWin, -# Darwin, MinGW, and NonStop. -# -# (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt -# within the Gradle project. -# -# You can find Gradle at https://github.com/gradle/gradle/. -# -############################################################################## - -# Attempt to set APP_HOME - -# Resolve links: $0 may be a link -app_path=$0 - -# Need this for daisy-chained symlinks. -while - APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path - [ -h "$app_path" ] -do - ls=$( ls -ld "$app_path" ) - link=${ls#*' -> '} - case $link in #( - /*) app_path=$link ;; #( - *) app_path=$APP_HOME$link ;; - esac -done - -# This is normally unused -# shellcheck disable=SC2034 -APP_BASE_NAME=${0##*/} -# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD=maximum - -warn () { - echo "$*" -} >&2 - -die () { - echo - echo "$*" - echo - exit 1 -} >&2 - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "$( uname )" in #( - CYGWIN* ) cygwin=true ;; #( - Darwin* ) darwin=true ;; #( - MSYS* | MINGW* ) msys=true ;; #( - NONSTOP* ) nonstop=true ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD=$JAVA_HOME/jre/sh/java - else - JAVACMD=$JAVA_HOME/bin/java - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD=java - if ! command -v java >/dev/null 2>&1 - then - die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -fi - -# Increase the maximum file descriptors if we can. -if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then - case $MAX_FD in #( - max*) - # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC2039,SC3045 - MAX_FD=$( ulimit -H -n ) || - warn "Could not query maximum file descriptor limit" - esac - case $MAX_FD in #( - '' | soft) :;; #( - *) - # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC2039,SC3045 - ulimit -n "$MAX_FD" || - warn "Could not set maximum file descriptor limit to $MAX_FD" - esac -fi - -# Collect all arguments for the java command, stacking in reverse order: -# * args from the command line -# * the main class name -# * -classpath -# * -D...appname settings -# * --module-path (only if needed) -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. - -# For Cygwin or MSYS, switch paths to Windows format before running java -if "$cygwin" || "$msys" ; then - APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) - - JAVACMD=$( cygpath --unix "$JAVACMD" ) - - # Now convert the arguments - kludge to limit ourselves to /bin/sh - for arg do - if - case $arg in #( - -*) false ;; # don't mess with options #( - /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath - [ -e "$t" ] ;; #( - *) false ;; - esac - then - arg=$( cygpath --path --ignore --mixed "$arg" ) - fi - # Roll the args list around exactly as many times as the number of - # args, so each arg winds up back in the position where it started, but - # possibly modified. - # - # NB: a `for` loop captures its iteration list before it begins, so - # changing the positional parameters here affects neither the number of - # iterations, nor the values presented in `arg`. - shift # remove old arg - set -- "$@" "$arg" # push replacement arg - done -fi - - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, -# and any embedded shellness will be escaped. -# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be -# treated as '${Hostname}' itself on the command line. - -set -- \ - "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ - "$@" - -# Stop when "xargs" is not available. -if ! command -v xargs >/dev/null 2>&1 -then - die "xargs is not available" -fi - -# Use "xargs" to parse quoted args. -# -# With -n1 it outputs one arg per line, with the quotes and backslashes removed. -# -# In Bash we could simply go: -# -# readarray ARGS < <( xargs -n1 <<<"$var" ) && -# set -- "${ARGS[@]}" "$@" -# -# but POSIX shell has neither arrays nor command substitution, so instead we -# post-process each arg (as a line of input to sed) to backslash-escape any -# character that might be a shell metacharacter, then use eval to reverse -# that process (while maintaining the separation between arguments), and wrap -# the whole thing up as a single "set" statement. -# -# This will of course break if any of these variables contains a newline or -# an unmatched quote. -# - -eval "set -- $( - printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | - xargs -n1 | - sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | - tr '\n' ' ' - )" '"$@"' - -exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat deleted file mode 100644 index 93e3f59..0000000 --- a/gradlew.bat +++ /dev/null @@ -1,92 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/lib/bld/bld-wrapper.jar b/lib/bld/bld-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..d17ad8982355ae5516129c6b07841609641eb492 GIT binary patch literal 27321 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 Kus>-qu>S(PSAA^& literal 0 HcmV?d00001 diff --git a/lib/bld/bld-wrapper.properties b/lib/bld/bld-wrapper.properties new file mode 100644 index 0000000..8375666 --- /dev/null +++ b/lib/bld/bld-wrapper.properties @@ -0,0 +1,10 @@ +bld.downloadExtensionJavadoc=false +bld.downloadExtensionSources=true +bld.extension-jacoco=com.uwyn.rife2:bld-jacoco-report:0.9.1 +bld.extensions=com.uwyn.rife2:bld-generated-version:0.9.3-SNAPSHOT +bld.extensions-kotlin=com.uwyn.rife2:bld-kotlin:0.9.0-SNAPSHOT +bld.extensions-pmd=com.uwyn.rife2:bld-testng:0.9.2 +bld.repositories=MAVEN_LOCAL,MAVEN_CENTRAL,RIFE2_SNAPSHOTS,RIFE2_RELEASES +bld.downloadLocation= +bld.sourceDirectories= +bld.version=1.7.5 diff --git a/release_info.txt b/release_info.txt new file mode 100644 index 0000000..f5efde2 --- /dev/null +++ b/release_info.txt @@ -0,0 +1,27 @@ +/* + * This file is automatically generated + * Do not modify! -- ALL CHANGES WILL BE ERASED! + */ + +package {{v packageName/}} + +import java.time.Instant +import java.time.LocalDateTime +import java.time.ZoneId + +/** + * Provides release information. + */ +object {{v className/}} { + const val PROJECT = "{{v project/}}" + const val VERSION = "{{v version/}}" + + @JvmField + val BUILD_DATE: LocalDateTime = LocalDateTime.ofInstant( + Instant.ofEpochMilli({{v epoch/}}L), ZoneId.systemDefault() + ) + + 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/settings.gradle b/settings.gradle deleted file mode 100644 index 53bc4db..0000000 --- a/settings.gradle +++ /dev/null @@ -1,18 +0,0 @@ -plugins { - id "com.gradle.enterprise" version "3.6.3" -} - -gradleEnterprise { - buildScan { - link("GitHub", "https://github.com/ethauvin/pinboard-poster/tree/master") - if (System.getenv("CI")) { - uploadInBackground = false - publishOnFailure() - tag "CI" - } - termsOfServiceUrl = "https://gradle.com/terms-of-service" - termsOfServiceAgree = "yes" - } -} - -rootProject.name = 'mobibot' diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 0000000..85d8fce --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,7 @@ +sonar.organization=ethauvin-github +sonar.projectKey=ethauvin_mobibot +sonar.coverage.jacoco.xmlReportPaths=build/reports/jacoco/test/jacocoTestReport.xml +sonar.sources=src/main/kotlin/ +sonar.tests=src/test/kotlin/ +sonar.java.binaries=build/main,build/test +sonar.java.libraries=lib/compile/*.jar diff --git a/src/bld/java/net/thauvin/erik/MobibotBuild.java b/src/bld/java/net/thauvin/erik/MobibotBuild.java new file mode 100644 index 0000000..9b6b32a --- /dev/null +++ b/src/bld/java/net/thauvin/erik/MobibotBuild.java @@ -0,0 +1,170 @@ +/* + * MobibotBuild.java + * + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package net.thauvin.erik; + +import rife.bld.BuildCommand; +import rife.bld.Project; +import rife.bld.dependencies.Repository; +import rife.bld.extension.CompileKotlinOperation; +import rife.bld.extension.CompileKotlinOptions; +import rife.bld.extension.GeneratedVersionOperation; +import rife.bld.extension.JacocoReportOperation; +import rife.tools.FileUtils; +import rife.tools.exceptions.FileUtilsErrorException; + +import java.io.File; +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import java.util.jar.Attributes; + +import static rife.bld.dependencies.Repository.MAVEN_CENTRAL; +import static rife.bld.dependencies.Repository.MAVEN_LOCAL; +import static rife.bld.dependencies.Scope.compile; +import static rife.bld.dependencies.Scope.test; + +public class MobibotBuild extends Project { + public MobibotBuild() { + pkg = "net.thauvin.erik.mobibot"; + name = "mobibot"; + version = version(0, 8, 0, "rc+" + + DateTimeFormatter.ofPattern("yyyyMMddHHmmss").format(LocalDateTime.now())); + + mainClass = pkg + ".Mobibot"; + + javaRelease = 17; + downloadSources = true; + autoDownloadPurge = true; + repositories = List.of(MAVEN_LOCAL, MAVEN_CENTRAL, new Repository("https://jitpack.io")); + + var log4j = version(2, 21, 1); + scope(compile) + // PircBotX + .include(dependency("com.github.pircbotx", "pircbotx", "2.3.1")) + // Commons (mostly for PircBotX) + .include(dependency("org.apache.commons", "commons-lang3", "3.13.0")) + .include(dependency("org.apache.commons", "commons-text", "1.11.0")) + .include(dependency("commons-codec", "commons-codec", "1.16.0")) + .include(dependency("commons-net", "commons-net", "3.10.0")) + // Google + .include(dependency("com.google.code.gson", "gson", "2.10.1")) + .include(dependency("com.google.guava", "guava", "32.1.3-jre")) + // Kotlin + .include(dependency("org.jetbrains.kotlin", "kotlin-stdlib", version(1, 9, 20))) + .include(dependency("org.jetbrains.kotlinx", "kotlinx-coroutines-core", "1.7.3")) + .include(dependency("org.jetbrains.kotlinx", "kotlinx-cli-jvm", "0.3.6")) + // Logging + .include(dependency("org.slf4j", "slf4j-api", "2.0.9")) + .include(dependency("org.apache.logging.log4j", "log4j-api", log4j)) + .include(dependency("org.apache.logging.log4j", "log4j-core", log4j)) + .include(dependency("org.apache.logging.log4j", "log4j-slf4j2-impl", log4j)) + .include(dependency("com.rometools", "rome", "2.1.0")) + .include(dependency("com.squareup.okhttp3", "okhttp", "4.12.0")) + .include(dependency("net.aksingh", "owm-japis", "2.5.3.0")) + .include(dependency("net.objecthunter", "exp4j", "0.4.8")) + .include(dependency("org.json", "json", "20231013")) + .include(dependency("org.jsoup", "jsoup", "1.16.2")) + // Thauvin + .include(dependency("net.thauvin.erik", "cryptoprice", "1.0.1")) + .include(dependency("net.thauvin.erik", "jokeapi", "0.9.0")) + .include(dependency("net.thauvin.erik", "pinboard-poster", "1.1.0")) + .include(dependency("net.thauvin.erik.urlencoder", "urlencoder-lib-jvm", "1.4.0")); + scope(test) + .include(dependency("com.willowtreeapps.assertk", "assertk-jvm", version(0, 27, 0))) + .include(dependency("org.jetbrains.kotlin", "kotlin-test-junit5", version(1, 9, 20))) + .include(dependency("org.junit.jupiter", "junit-jupiter", version(5, 10, 1))) + .include(dependency("org.junit.platform", "junit-platform-console-standalone", version(1, 10, 1))); + + List jars = new ArrayList<>(); + compileClasspathJars().forEach(f -> jars.add("./lib/" + f.getName())); + jarOperation() + .manifestAttribute(Attributes.Name.MAIN_CLASS, mainClass()) + .manifestAttribute(Attributes.Name.CLASS_PATH, ". " + String.join(" ", jars)); + + jarSourcesOperation().sourceDirectories(new File(srcMainDirectory(), "kotlin")); + } + + public static void main(String[] args) { + new MobibotBuild().start(args); + } + + @Override + public void clean() throws Exception { + FileUtils.deleteDirectory(new File("deploy")); + super.clean(); + } + + @BuildCommand(summary = "Compiles the Kotlin project") + @Override + public void compile() throws Exception { + releaseInfo(); + new CompileKotlinOperation() + .fromProject(this) + .compileOptions( + new CompileKotlinOptions() + .jdkRelease(javaRelease) + .verbose(true) + ) + .execute(); + } + + @BuildCommand(summary = "Copies all needed files to the deploy directory") + public void deploy() throws FileUtilsErrorException { + var deploy = new File("deploy"); + var lib = new File(deploy, "lib"); + var ignore = lib.mkdirs(); + FileUtils.copyDirectory(new File("properties"), deploy); + FileUtils.copyDirectory(libCompileDirectory(), lib); + FileUtils.copy(new File(buildDistDirectory(), jarFileName()), new File(deploy, "mobibot.jar")); + } + + @BuildCommand(summary = "Generates JaCoCo Reports") + public void jacoco() throws IOException { + new JacocoReportOperation() + .fromProject(this) + .execute(); + } + + @BuildCommand(value = "release-info", summary = "Generates the ReleaseInfo class") + public void releaseInfo() { + new GeneratedVersionOperation() + .fromProject(this) + .classTemplate(new File(workDirectory(), "release-info.txt")) + .className("ReleaseInfo") + .packageName(pkg) + .directory(new File(srcMainDirectory(), "kotlin")) + .extension(".kt") + .execute(); + } +} diff --git a/src/main/java/net/thauvin/erik/mobibot/modules/War.java b/src/main/java/net/thauvin/erik/mobibot/modules/War.java deleted file mode 100644 index 4bbbd9b..0000000 --- a/src/main/java/net/thauvin/erik/mobibot/modules/War.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * War.java - * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * Neither the name of this project nor the names of its contributors may be - * used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE - * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR - * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, - * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package net.thauvin.erik.mobibot.modules; - -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 - * @since 1.0 - */ -public final class War extends AbstractModule { - private static final String[] CLUBS = - {"🃑", "🃞", "🃝", "🃛", "🃚", "🃙", "🃘", "🃗", "🃖", "🃕", "🃔", "🃓", "🃒"}; - private static final String[] DIAMONDS = - {"🃁", "🃎", "🃍", "🃋", "🃊", "🃉", "🃈", "🃇", "🃆", "🃅", "🃄", "🃃", "🃂"}; - private static final String[] HEARTS = - {"🂱", "🂾", "🂽", "🂻", "🂺", "🂹", "🂸", "🂷", "🂶", "🂵", "🂴", "🂳", "🂲"}; - // Random - private static final SecureRandom RANDOM = new SecureRandom(); - private static final String[] SPADES = - {"🂡", "🂮", "🂭", "🂫", "🂪", "🂩", "🂨", "🂧", "🂦", "🂥", "🂤", "🂣", "🂢"}; - private static final String[][] DECK = {HEARTS, SPADES, DIAMONDS, CLUBS}; - // War command - private static final String WAR_CMD = "war"; - - /** - * 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(@NotNull final String channel, @NotNull final String cmd, @NotNull final String args, - @NotNull final GenericMessageEvent event) { - int i; - int y; - - do { - i = RANDOM.nextInt(HEARTS.length); - y = RANDOM.nextInt(HEARTS.length); - - final String result; - if (i < y) { - result = bold("win"); - } else if (i > y) { - result = bold("lose"); - } else { - result = bold("tie") + ". This means " + bold("WAR"); - } - - event.respond(DECK[RANDOM.nextInt(DECK.length)][i] + " " + DECK[RANDOM.nextInt(DECK.length)][y] + - " » You " + result + '!'); - - } while (i == y); - } -} diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/Addons.kt b/src/main/kotlin/net/thauvin/erik/mobibot/Addons.kt index 2c5f05d..c6f16cb 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/Addons.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/Addons.kt @@ -1,7 +1,7 @@ /* * Addons.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/Constants.kt b/src/main/kotlin/net/thauvin/erik/mobibot/Constants.kt index 98ef74a..ea89f4c 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/Constants.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/Constants.kt @@ -1,7 +1,7 @@ /* * Constants.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/FeedReader.kt b/src/main/kotlin/net/thauvin/erik/mobibot/FeedReader.kt index d82f011..371b523 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/FeedReader.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/FeedReader.kt @@ -1,7 +1,7 @@ /* * FeedReader.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/Mobibot.kt b/src/main/kotlin/net/thauvin/erik/mobibot/Mobibot.kt index f91c457..47dd7c2 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/Mobibot.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/Mobibot.kt @@ -1,7 +1,7 @@ /* * Mobibot.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -51,7 +51,6 @@ import net.thauvin.erik.mobibot.commands.links.* import net.thauvin.erik.mobibot.commands.seen.Seen import net.thauvin.erik.mobibot.commands.tell.Tell import net.thauvin.erik.mobibot.modules.* -import net.thauvin.erik.semver.Version import org.pircbotx.Configuration import org.pircbotx.PircBotX import org.pircbotx.hooks.ListenerAdapter @@ -66,7 +65,6 @@ import java.util.* import java.util.regex.Pattern import kotlin.system.exitProcess -@Version(properties = "version.properties", className = "ReleaseInfo", template = "version.mustache", type = "kt") class Mobibot(nickname: String, val channel: String, logsDirPath: String, p: Properties) : ListenerAdapter() { // The bot configuration. private val config: Configuration @@ -257,7 +255,7 @@ class Mobibot(nickname: String, val channel: String, logsDirPath: String, p: Pro // Output the version println( "${ReleaseInfo.PROJECT.capitalise()} ${ReleaseInfo.VERSION}" + - " (${ReleaseInfo.BUILDDATE.toIsoLocalDate()})" + " (${ReleaseInfo.BUILD_DATE.toIsoLocalDate()})" ) println(ReleaseInfo.WEBSITE) } else { diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/Pinboard.kt b/src/main/kotlin/net/thauvin/erik/mobibot/Pinboard.kt index 7cb5aed..ac01b0a 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/Pinboard.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/Pinboard.kt @@ -1,7 +1,7 @@ /* * Pinboard.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/ReleaseInfo.kt b/src/main/kotlin/net/thauvin/erik/mobibot/ReleaseInfo.kt new file mode 100644 index 0000000..88634ae --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/mobibot/ReleaseInfo.kt @@ -0,0 +1,27 @@ +/* + * This file is automatically generated + * Do not modify! -- ALL CHANGES WILL BE ERASED! + */ + +package net.thauvin.erik.mobibot + +import java.time.Instant +import java.time.LocalDateTime +import java.time.ZoneId + +/** + * Provides release information. + */ +object ReleaseInfo { + const val PROJECT = "mobibot" + const val VERSION = "0.8.0-rc+20231110234054" + + @JvmField + val BUILD_DATE: LocalDateTime = LocalDateTime.ofInstant( + Instant.ofEpochMilli(1699688454937L), ZoneId.systemDefault() + ) + + 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/src/main/kotlin/net/thauvin/erik/mobibot/Utils.kt b/src/main/kotlin/net/thauvin/erik/mobibot/Utils.kt index e4760d2..d69c0c5 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/Utils.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/Utils.kt @@ -1,7 +1,7 @@ /* * Utils.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/commands/AbstractCommand.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/AbstractCommand.kt index 5f79472..e0b091a 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/commands/AbstractCommand.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/AbstractCommand.kt @@ -1,7 +1,7 @@ /* * AbstractCommand.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/commands/ChannelFeed.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/ChannelFeed.kt index 038e378..9084660 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/commands/ChannelFeed.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/ChannelFeed.kt @@ -1,7 +1,7 @@ /* * ChannelFeed.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/commands/Cycle.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Cycle.kt index 9608ca8..18a8b1d 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/commands/Cycle.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Cycle.kt @@ -1,7 +1,7 @@ /* * Cycle.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/commands/Die.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Die.kt index f271bfa..c1708a9 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/commands/Die.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Die.kt @@ -1,7 +1,7 @@ /* * Die.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/commands/Ignore.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Ignore.kt index d083c10..8d2b154 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/commands/Ignore.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Ignore.kt @@ -1,7 +1,7 @@ /* * Ignore.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/commands/Info.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Info.kt index ed0b6ef..8ee8c4f 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/commands/Info.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Info.kt @@ -1,7 +1,7 @@ /* * Info.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/commands/Me.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Me.kt index ec7823b..f2a13ba 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/commands/Me.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Me.kt @@ -1,7 +1,7 @@ /* * Me.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/commands/Modules.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Modules.kt index b2293b0..1aaefd7 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/commands/Modules.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Modules.kt @@ -1,7 +1,7 @@ /* * Modules.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/commands/Msg.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Msg.kt index 20a6635..0f492c9 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/commands/Msg.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Msg.kt @@ -1,7 +1,7 @@ /* * Msg.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/commands/Nick.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Nick.kt index 85a03ab..41b2c4b 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/commands/Nick.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Nick.kt @@ -1,7 +1,7 @@ /* * Nick.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/commands/Recap.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Recap.kt index 77154c7..0159017 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/commands/Recap.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Recap.kt @@ -1,7 +1,7 @@ /* * Recap.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/commands/Say.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Say.kt index 7f76d35..1f89982 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/commands/Say.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Say.kt @@ -1,7 +1,7 @@ /* * Say.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/commands/Users.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Users.kt index 33d6fef..4a881a7 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/commands/Users.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Users.kt @@ -1,7 +1,7 @@ /* * Users.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/commands/Versions.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Versions.kt index 896c569..a8300f0 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/commands/Versions.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Versions.kt @@ -1,7 +1,7 @@ /* * Versions.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -40,7 +40,7 @@ import org.pircbotx.hooks.types.GenericMessageEvent class Versions : AbstractCommand() { private val allVersions = listOf( - "Version: ${ReleaseInfo.VERSION} (${ReleaseInfo.BUILDDATE.toIsoLocalDate()})", + "Version: ${ReleaseInfo.VERSION} (${ReleaseInfo.BUILD_DATE.toIsoLocalDate()})", "${System.getProperty("os.name")} ${System.getProperty("os.version")} (${System.getProperty("os.arch")})" + ", JVM ${System.getProperty("java.runtime.version")}", "Kotlin ${KotlinVersion.CURRENT}, PircBotX ${PircBotX.VERSION}" 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 index 1443d44..90d8bb3 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/commands/links/Comment.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/links/Comment.kt @@ -1,7 +1,7 @@ /* * Comment.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/commands/links/LinksManager.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/links/LinksManager.kt index fba6b99..2d7add6 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/commands/links/LinksManager.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/links/LinksManager.kt @@ -1,7 +1,7 @@ /* * LinksManager.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: 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 index ff4278d..dca99cf 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/commands/links/Posting.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/links/Posting.kt @@ -1,7 +1,7 @@ /* * Posting.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: 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 index 1662857..c59cad9 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/commands/links/Tags.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/links/Tags.kt @@ -1,7 +1,7 @@ /* * Tags.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: 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 index 825e374..1626a58 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/commands/links/View.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/links/View.kt @@ -1,7 +1,7 @@ /* * View.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/commands/seen/NickComparator.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/seen/NickComparator.kt index cfd2c27..2a3856b 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/commands/seen/NickComparator.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/seen/NickComparator.kt @@ -1,7 +1,7 @@ /* * NickComparator.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/commands/seen/Seen.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/seen/Seen.kt index c9ee0f3..83083fd 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/commands/seen/Seen.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/seen/Seen.kt @@ -1,7 +1,7 @@ /* * Seen.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/commands/seen/SeenNick.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/seen/SeenNick.kt index 7924977..1445f9f 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/commands/seen/SeenNick.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/seen/SeenNick.kt @@ -1,7 +1,7 @@ /* * SeenNick.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: 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 index 061ca6a..e9d18ae 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/commands/tell/Tell.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/tell/Tell.kt @@ -1,7 +1,7 @@ /* * Tell.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/commands/tell/TellManager.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/tell/TellManager.kt index b65a4da..229bc5f 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/commands/tell/TellManager.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/tell/TellManager.kt @@ -1,7 +1,7 @@ /* * TellManager.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: 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 index d17fbb5..cc48d59 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/commands/tell/TellMessage.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/tell/TellMessage.kt @@ -1,7 +1,7 @@ /* * TellMessage.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/entries/Entries.kt b/src/main/kotlin/net/thauvin/erik/mobibot/entries/Entries.kt index e8676ec..15506e0 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/entries/Entries.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/entries/Entries.kt @@ -1,7 +1,7 @@ /* * Entries.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/entries/EntriesUtils.kt b/src/main/kotlin/net/thauvin/erik/mobibot/entries/EntriesUtils.kt index 9c09626..6e58fd9 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/entries/EntriesUtils.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/entries/EntriesUtils.kt @@ -1,7 +1,7 @@ /* * EntriesUtils.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/entries/EntryComment.kt b/src/main/kotlin/net/thauvin/erik/mobibot/entries/EntryComment.kt index e18d692..c0cc2a8 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/entries/EntryComment.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/entries/EntryComment.kt @@ -1,7 +1,7 @@ /* * EntryComment.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/entries/EntryLink.kt b/src/main/kotlin/net/thauvin/erik/mobibot/entries/EntryLink.kt index 4a69446..8098bbf 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/entries/EntryLink.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/entries/EntryLink.kt @@ -1,7 +1,7 @@ /* * EntryLink.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/entries/FeedsManager.kt b/src/main/kotlin/net/thauvin/erik/mobibot/entries/FeedsManager.kt index f786cb2..e7017ab 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/entries/FeedsManager.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/entries/FeedsManager.kt @@ -1,7 +1,7 @@ /* * FeedsManager.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/modules/AbstractModule.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/AbstractModule.kt index 8c8e736..8dcf6d8 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/modules/AbstractModule.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/AbstractModule.kt @@ -1,7 +1,7 @@ /* * AbstractModule.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/modules/Calc.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/Calc.kt index b7aae28..9003783 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/modules/Calc.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/Calc.kt @@ -1,7 +1,7 @@ /* * Calc.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/modules/ChatGpt.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/ChatGpt.kt index bd92332..09791a8 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/modules/ChatGpt.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/ChatGpt.kt @@ -1,7 +1,7 @@ /* * ChatGpt.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/modules/CryptoPrices.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/CryptoPrices.kt index d14056e..4464732 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/modules/CryptoPrices.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/CryptoPrices.kt @@ -1,7 +1,7 @@ /* * CryptoPrices.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -120,7 +120,7 @@ class CryptoPrices : AbstractModule() { } /** - * Loads the Fiat currencies.. + * Loads the Fiat currencies. */ @JvmStatic @Throws(ModuleException::class) diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/modules/CurrencyConverter.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/CurrencyConverter.kt index da0efd8..954f4d1 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/modules/CurrencyConverter.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/CurrencyConverter.kt @@ -1,7 +1,7 @@ /* * CurrencyConverter.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/modules/Dice.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/Dice.kt index 8420fb1..84f2280 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/modules/Dice.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/Dice.kt @@ -1,7 +1,7 @@ /* * Dice.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/modules/GoogleSearch.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/GoogleSearch.kt index f426d1e..a9b15a5 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/modules/GoogleSearch.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/GoogleSearch.kt @@ -1,7 +1,7 @@ /* * GoogleSearch.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/modules/Joke.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/Joke.kt index 2760fa7..91b9ad2 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/modules/Joke.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/Joke.kt @@ -1,7 +1,7 @@ /* * Joke.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/modules/Lookup.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/Lookup.kt index 9ab2ead..eecc55e 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/modules/Lookup.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/Lookup.kt @@ -1,7 +1,7 @@ /* * Lookup.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/modules/Mastodon.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/Mastodon.kt index 3be3a5f..1141c0e 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/modules/Mastodon.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/Mastodon.kt @@ -1,7 +1,7 @@ /* * Mastodon.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/modules/ModuleException.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/ModuleException.kt index a7416c2..9a0e641 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/modules/ModuleException.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/ModuleException.kt @@ -1,7 +1,7 @@ /* * ModuleException.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/modules/Ping.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/Ping.kt index 944dbc1..86b311a 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/modules/Ping.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/Ping.kt @@ -1,7 +1,7 @@ /* * Ping.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/modules/RockPaperScissors.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/RockPaperScissors.kt index a299d8d..d224568 100644 --- a/src/main/kotlin/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 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/modules/StockQuote.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/StockQuote.kt index dcae5e7..6b591cd 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/modules/StockQuote.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/StockQuote.kt @@ -1,7 +1,7 @@ /* * StockQuote.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/modules/War.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/War.kt new file mode 100644 index 0000000..0c64465 --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/War.kt @@ -0,0 +1,89 @@ +/* + * War.kt + * + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.thauvin.erik.mobibot.modules + +import net.thauvin.erik.mobibot.Utils.bold +import net.thauvin.erik.mobibot.Utils.helpFormat +import org.pircbotx.hooks.types.GenericMessageEvent +import java.security.SecureRandom + +/** + * The War module. + * + * @author [Erik C. Thauvin](https://erik.thauvin.net/) + * @since 1.0 + */ +class War : AbstractModule() { + override val name = "War" + + override fun commandResponse( + channel: String, cmd: String, args: String, + event: GenericMessageEvent + ) { + var i: Int + var y: Int + do { + i = RANDOM.nextInt(HEARTS.size) + y = RANDOM.nextInt(HEARTS.size) + val result: String = if (i < y) { + "win".bold() + } else if (i > y) { + "lose".bold() + } else { + "tie".bold() + ". This means " + "WAR".bold() + } + event.respond( + DECK[RANDOM.nextInt(DECK.size)][i] + " " + DECK[RANDOM.nextInt(DECK.size)][y] + + " » You " + result + '!' + ) + } while (i == y) + } + + companion object { + private val CLUBS = arrayOf("🃑", "🃞", "🃝", "🃛", "🃚", "🃙", "🃘", "🃗", "🃖", "🃕", "🃔", "🃓", "🃒") + private val DIAMONDS = arrayOf("🃁", "🃎", "🃍", "🃋", "🃊", "🃉", "🃈", "🃇", "🃆", "🃅", "🃄", "🃃", "🃂") + private val HEARTS = arrayOf("🂱", "🂾", "🂽", "🂻", "🂺", "🂹", "🂸", "🂷", "🂶", "🂵", "🂴", "🂳", "🂲") + + // Random + private val RANDOM = SecureRandom() + private val SPADES = arrayOf("🂡", "🂮", "🂭", "🂫", "🂪", "🂩", "🂨", "🂧", "🂦", "🂥", "🂤", "🂣", "🂢") + private val DECK = arrayOf(HEARTS, SPADES, DIAMONDS, CLUBS) + + // War command + private const val WAR_CMD = "war" + } + + init { + commands.add(WAR_CMD) + help.add("To play war:") + help.add(helpFormat("%c $WAR_CMD")) + } +} diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/modules/Weather2.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/Weather2.kt index 80a06fa..83eba95 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/modules/Weather2.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/Weather2.kt @@ -1,7 +1,7 @@ /* * Weather2.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/modules/WolframAlpha.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/WolframAlpha.kt index a72efab..8d9c4e6 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/modules/WolframAlpha.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/WolframAlpha.kt @@ -1,7 +1,7 @@ /* * WolframAlpha.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/modules/WorldTime.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/WorldTime.kt index 18072bc..0c379b3 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/modules/WorldTime.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/WorldTime.kt @@ -1,7 +1,7 @@ /* * WorldTime.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/msg/ErrorMessage.kt b/src/main/kotlin/net/thauvin/erik/mobibot/msg/ErrorMessage.kt index 0607936..b9f5212 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/msg/ErrorMessage.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/msg/ErrorMessage.kt @@ -1,7 +1,7 @@ /* * ErrorMessage.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/msg/Message.kt b/src/main/kotlin/net/thauvin/erik/mobibot/msg/Message.kt index 23a33b9..84e3a1e 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/msg/Message.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/msg/Message.kt @@ -1,7 +1,7 @@ /* * Message.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -30,8 +30,6 @@ */ package net.thauvin.erik.mobibot.msg -import net.thauvin.erik.semver.Constants - /** * The `Message` class. */ @@ -43,7 +41,7 @@ open class Message @JvmOverloads constructor( var isPrivate: Boolean = false ) { companion object { - var DEFAULT_COLOR = Constants.EMPTY + var DEFAULT_COLOR = "" } init { diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/msg/NoticeMessage.kt b/src/main/kotlin/net/thauvin/erik/mobibot/msg/NoticeMessage.kt index 037d504..2be52aa 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/msg/NoticeMessage.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/msg/NoticeMessage.kt @@ -1,7 +1,7 @@ /* * NoticeMessage.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/msg/PrivateMessage.kt b/src/main/kotlin/net/thauvin/erik/mobibot/msg/PrivateMessage.kt index b424fdf..8d34d28 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/msg/PrivateMessage.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/msg/PrivateMessage.kt @@ -1,7 +1,7 @@ /* * PrivateMessage.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/msg/PublicMessage.kt b/src/main/kotlin/net/thauvin/erik/mobibot/msg/PublicMessage.kt index 9c5e088..9e67ecb 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/msg/PublicMessage.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/msg/PublicMessage.kt @@ -1,7 +1,7 @@ /* * PublicMessage.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/social/SocialManager.kt b/src/main/kotlin/net/thauvin/erik/mobibot/social/SocialManager.kt index 91f2dd9..3fb3874 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/social/SocialManager.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/social/SocialManager.kt @@ -1,7 +1,7 @@ /* * SocialManager.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/social/SocialModule.kt b/src/main/kotlin/net/thauvin/erik/mobibot/social/SocialModule.kt index b594670..47a33d9 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/social/SocialModule.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/social/SocialModule.kt @@ -1,7 +1,7 @@ /* * SocialModule.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/social/SocialTimer.kt b/src/main/kotlin/net/thauvin/erik/mobibot/social/SocialTimer.kt index 3fd315e..e779a3c 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/social/SocialTimer.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/social/SocialTimer.kt @@ -1,7 +1,7 @@ /* * SocialTimer.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml new file mode 100644 index 0000000..16644cc --- /dev/null +++ b/src/main/resources/log4j2.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/AddonsTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/AddonsTest.kt index 3241bf0..7f65548 100644 --- a/src/test/kotlin/net/thauvin/erik/mobibot/AddonsTest.kt +++ b/src/test/kotlin/net/thauvin/erik/mobibot/AddonsTest.kt @@ -1,7 +1,7 @@ /* * AddonsTest.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -42,8 +42,8 @@ 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.* -import org.testng.annotations.Test import java.util.* +import kotlin.test.Test class AddonsTest { private val p = Properties().apply { diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/DisableOnCi.kt b/src/test/kotlin/net/thauvin/erik/mobibot/DisableOnCi.kt new file mode 100644 index 0000000..01d98ca --- /dev/null +++ b/src/test/kotlin/net/thauvin/erik/mobibot/DisableOnCi.kt @@ -0,0 +1,44 @@ +/* + * DisableOnCi.kt + * + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.thauvin.erik.mobibot + +import org.junit.jupiter.api.extension.ExtendWith + +/** + * Disables tests on CI annotation. + * + * @author [Erik C. Thauvin](https://erik.thauvin.net/) + * @since 1.0 + */ +@Target(AnnotationTarget.TYPE, AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +@ExtendWith(DisableOnCiCondition::class) +annotation class DisabledOnCi diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/DisableOnCiCondition.kt b/src/test/kotlin/net/thauvin/erik/mobibot/DisableOnCiCondition.kt new file mode 100644 index 0000000..24f1ca0 --- /dev/null +++ b/src/test/kotlin/net/thauvin/erik/mobibot/DisableOnCiCondition.kt @@ -0,0 +1,51 @@ +/* + * DisableOnCiCondition.kt + * + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.thauvin.erik.mobibot + +import org.junit.jupiter.api.extension.ConditionEvaluationResult +import org.junit.jupiter.api.extension.ExecutionCondition +import org.junit.jupiter.api.extension.ExtensionContext + +/** + * Disables tests on CI condition. + * + * @author [Erik C. Thauvin](https://erik.thauvin.net/) + * @since 1.0 + */ +class DisableOnCiCondition : ExecutionCondition { + override fun evaluateExecutionCondition(context: ExtensionContext): ConditionEvaluationResult { + return if (System.getenv("CI") != null) { + ConditionEvaluationResult.disabled("Test disabled on CI") + } else { + ConditionEvaluationResult.enabled("Test enabled") + } + } +} diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/ExceptionSanitizer.kt b/src/test/kotlin/net/thauvin/erik/mobibot/ExceptionSanitizer.kt index a3994ec..b02188e 100644 --- a/src/test/kotlin/net/thauvin/erik/mobibot/ExceptionSanitizer.kt +++ b/src/test/kotlin/net/thauvin/erik/mobibot/ExceptionSanitizer.kt @@ -1,7 +1,7 @@ /* * ExceptionSanitizer.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/FeedReaderTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/FeedReaderTest.kt index 78f5b18..cfc3d4e 100644 --- a/src/test/kotlin/net/thauvin/erik/mobibot/FeedReaderTest.kt +++ b/src/test/kotlin/net/thauvin/erik/mobibot/FeedReaderTest.kt @@ -1,7 +1,7 @@ /* * FeedReaderTest.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -37,10 +37,10 @@ import assertk.assertions.* import com.rometools.rome.io.FeedException import net.thauvin.erik.mobibot.FeedReader.Companion.readFeed import net.thauvin.erik.mobibot.msg.Message -import org.testng.annotations.Test import java.io.IOException import java.net.MalformedURLException import java.net.UnknownHostException +import kotlin.test.Test /** * The `FeedReader Test` class. diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/LocalProperties.kt b/src/test/kotlin/net/thauvin/erik/mobibot/LocalProperties.kt index 1384a72..95b6c08 100644 --- a/src/test/kotlin/net/thauvin/erik/mobibot/LocalProperties.kt +++ b/src/test/kotlin/net/thauvin/erik/mobibot/LocalProperties.kt @@ -1,7 +1,7 @@ /* * LocalProperties.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -30,7 +30,6 @@ */ package net.thauvin.erik.mobibot -import org.testng.annotations.BeforeSuite import java.io.IOException import java.net.InetAddress import java.net.UnknownHostException @@ -42,8 +41,7 @@ import java.util.* * Access to `local.properties`. */ open class LocalProperties { - @BeforeSuite(alwaysRun = true) - fun loadProperties() { + init { val localPath = Paths.get("local.properties") if (Files.exists(localPath)) { try { diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/PinboardTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/PinboardTest.kt index 87617e8..cf9d59a 100644 --- a/src/test/kotlin/net/thauvin/erik/mobibot/PinboardTest.kt +++ b/src/test/kotlin/net/thauvin/erik/mobibot/PinboardTest.kt @@ -1,7 +1,7 @@ /* * PinboardTest.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -34,10 +34,10 @@ package net.thauvin.erik.mobibot import net.thauvin.erik.mobibot.Utils.encodeUrl import net.thauvin.erik.mobibot.Utils.reader 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 +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue class PinboardTest : LocalProperties() { private val pinboard = Pinboard() diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/UtilsTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/UtilsTest.kt index 7a3f5d2..a024cff 100644 --- a/src/test/kotlin/net/thauvin/erik/mobibot/UtilsTest.kt +++ b/src/test/kotlin/net/thauvin/erik/mobibot/UtilsTest.kt @@ -1,7 +1,7 @@ /* * UtilsTest.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -59,14 +59,14 @@ import net.thauvin.erik.mobibot.Utils.today import net.thauvin.erik.mobibot.Utils.underline import net.thauvin.erik.mobibot.Utils.unescapeXml import net.thauvin.erik.mobibot.msg.Message.Companion.DEFAULT_COLOR +import org.junit.jupiter.api.BeforeEach 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.* +import kotlin.test.Test /** * The `Utils Test` class. @@ -78,7 +78,7 @@ class UtilsTest { private val localDateTime = LocalDateTime.of(1952, 2, 17, 12, 30, 0) private val test = "This is a test." - @BeforeClass + @BeforeEach fun setUp() { cal[1952, Calendar.FEBRUARY, 17, 12, 30] = 0 } diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/commands/InfoTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/commands/InfoTest.kt index 265009b..33a411f 100644 --- a/src/test/kotlin/net/thauvin/erik/mobibot/commands/InfoTest.kt +++ b/src/test/kotlin/net/thauvin/erik/mobibot/commands/InfoTest.kt @@ -1,7 +1,7 @@ /* * InfoTest.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -34,10 +34,10 @@ package net.thauvin.erik.mobibot.commands import assertk.assertThat import assertk.assertions.isEqualTo import net.thauvin.erik.mobibot.commands.Info.Companion.toUptime -import org.testng.annotations.Test +import kotlin.test.Test class InfoTest { - @Test(groups = ["commands"]) + @Test fun testToUptime() { assertThat( 547800300076L.toUptime(), diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/commands/RecapTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/commands/RecapTest.kt index f1fbe11..34dce0e 100644 --- a/src/test/kotlin/net/thauvin/erik/mobibot/commands/RecapTest.kt +++ b/src/test/kotlin/net/thauvin/erik/mobibot/commands/RecapTest.kt @@ -1,7 +1,7 @@ /* * RecapTest.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -37,10 +37,10 @@ import assertk.assertions.isEqualTo import assertk.assertions.matches import assertk.assertions.prop import assertk.assertions.size -import org.testng.annotations.Test +import kotlin.test.Test class RecapTest { - @Test(groups = ["commands"]) + @Test fun storeRecapTest() { for (i in 1..20) { Recap.storeRecap("sender$i", "test $i", false) diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/commands/links/LinksManagerTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/commands/links/LinksManagerTest.kt index 8e49b5e..eea09db 100644 --- a/src/test/kotlin/net/thauvin/erik/mobibot/commands/links/LinksManagerTest.kt +++ b/src/test/kotlin/net/thauvin/erik/mobibot/commands/links/LinksManagerTest.kt @@ -1,7 +1,7 @@ /* * LinksManagerTest.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -38,12 +38,12 @@ import assertk.assertions.isEqualTo import assertk.assertions.isTrue import assertk.assertions.size import net.thauvin.erik.mobibot.Constants -import org.testng.annotations.Test +import kotlin.test.Test class LinksManagerTest { private val linksManager = LinksManager() - @Test(groups = ["commands", "links"]) + @Test fun fetchTitle() { assertThat(linksManager.fetchTitle("https://erik.thauvin.net/"), "fetchTitle(Erik)").contains("Erik's Weblog") assertThat( @@ -52,13 +52,13 @@ class LinksManagerTest { ).isEqualTo(Constants.NO_TITLE) } - @Test(groups = ["commands", "links"]) + @Test fun testMatches() { assertThat(linksManager.matches("https://www.example.com/"), "matches(url)").isTrue() assertThat(linksManager.matches("HTTP://erik.thauvin.net/blog/ Erik's Weblog"), "matches(HTTP)").isTrue() } - @Test(groups = ["commands", "links"]) + @Test fun matchTagKeywordsTest() { linksManager.setProperty(LinksManager.KEYWORDS_PROP, "key1 key2,key3") val tags = mutableListOf() 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 index c28090d..b5028e3 100644 --- a/src/test/kotlin/net/thauvin/erik/mobibot/commands/links/ViewTest.kt +++ b/src/test/kotlin/net/thauvin/erik/mobibot/commands/links/ViewTest.kt @@ -1,7 +1,7 @@ /* * ViewTest.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -36,10 +36,10 @@ import assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.prop import net.thauvin.erik.mobibot.entries.EntryLink -import org.testng.annotations.Test +import kotlin.test.Test class ViewTest { - @Test(groups = ["commands", "links"]) + @Test fun testParseArgs() { val view = View() diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/commands/seen/SeenTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/commands/seen/SeenTest.kt index 52a21cc..331f97c 100644 --- a/src/test/kotlin/net/thauvin/erik/mobibot/commands/seen/SeenTest.kt +++ b/src/test/kotlin/net/thauvin/erik/mobibot/commands/seen/SeenTest.kt @@ -1,7 +1,7 @@ /* * SeenTest.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -34,29 +34,16 @@ package net.thauvin.erik.mobibot.commands.seen import assertk.all import assertk.assertThat import assertk.assertions.* -import org.testng.annotations.AfterClass -import org.testng.annotations.BeforeClass -import org.testng.annotations.Test +import org.junit.AfterClass +import org.junit.BeforeClass +import org.junit.jupiter.api.Order import kotlin.io.path.deleteIfExists import kotlin.io.path.fileSize +import kotlin.test.Test class SeenTest { - private val tmpFile = kotlin.io.path.createTempFile(suffix = ".ser") - private val seen = Seen(tmpFile.toAbsolutePath().toString()) - private val nick = "ErikT" - - @BeforeClass - fun saveTest() { - seen.add("ErikT") - assertThat(tmpFile.fileSize(), "tmpFile.size").isGreaterThan(0) - } - - @AfterClass(alwaysRun = true) - fun afterClass() { - tmpFile.deleteIfExists() - } - - @Test(priority = 1, groups = ["commands"]) + @Test + @Order(1) fun loadTest() { seen.clear() assertThat(seen::seenNicks).isEmpty() @@ -64,7 +51,8 @@ class SeenTest { assertThat(seen::seenNicks).key(nick).isNotNull() } - @Test(groups = ["commands"]) + @Test + @Order(2) fun addTest() { val last = seen.seenNicks[nick]?.lastSeen seen.add(nick.lowercase()) @@ -75,11 +63,31 @@ class SeenTest { } } - @Test(priority = 10, groups = ["commands"]) + @Test + @Order(3) fun clearTest() { seen.clear() seen.save() seen.load() assertThat(seen::seenNicks).size().isEqualTo(0) } + + companion object { + private val tmpFile = kotlin.io.path.createTempFile(suffix = ".ser") + private val seen = Seen(tmpFile.toAbsolutePath().toString()) + private const val nick = "ErikT" + + @JvmStatic + @BeforeClass + fun beforeClass() { + seen.add(nick) + assertThat(tmpFile.fileSize(), "tmpFile.size").isGreaterThan(0) + } + + @JvmStatic + @AfterClass + fun afterClass() { + tmpFile.deleteIfExists() + } + } } 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 index f7239e0..f2c6f35 100644 --- a/src/test/kotlin/net/thauvin/erik/mobibot/commands/tell/TellMessageTest.kt +++ b/src/test/kotlin/net/thauvin/erik/mobibot/commands/tell/TellMessageTest.kt @@ -1,7 +1,7 @@ /* * TellMessageTest.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -36,10 +36,10 @@ 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 +import kotlin.test.Test /** * The `TellMessageTest` class. @@ -49,7 +49,7 @@ class TellMessageTest { return Duration.between(date, LocalDateTime.now()).toMinutes() < 1 } - @Test(groups = ["commands", "tell"]) + @Test fun testTellMessage() { val message = "Test message." val recipient = "recipient" 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 index 115e9fb..9ab1bce 100644 --- a/src/test/kotlin/net/thauvin/erik/mobibot/commands/tell/TellMessagesMgrTest.kt +++ b/src/test/kotlin/net/thauvin/erik/mobibot/commands/tell/TellMessagesMgrTest.kt @@ -1,7 +1,7 @@ /* * TellMessagesMgrTest.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -34,16 +34,14 @@ package net.thauvin.erik.mobibot.commands.tell import assertk.all import assertk.assertThat import assertk.assertions.* -import org.testng.annotations.AfterClass -import org.testng.annotations.BeforeClass -import org.testng.annotations.Test +import org.junit.AfterClass import java.time.LocalDateTime import kotlin.io.path.createTempFile import kotlin.io.path.deleteIfExists import kotlin.io.path.fileSize +import kotlin.test.Test class TellMessagesMgrTest { - private val testFile = createTempFile(suffix = ".ser") private val maxDays = 10L private val testMessages = mutableListOf().apply { for (i in 0..5) { @@ -51,18 +49,12 @@ class TellMessagesMgrTest { } } - @BeforeClass - fun saveTest() { + init { TellManager.save(testFile.toAbsolutePath().toString(), testMessages) assertThat(testFile.fileSize()).isGreaterThan(0) } - @AfterClass - fun afterClass() { - testFile.deleteIfExists() - } - - @Test(groups = ["commands", "tell"]) + @Test fun cleanTest() { testMessages.add(TellMessage("sender", "recipient", "message").apply { queued = LocalDateTime.now().minusDays(maxDays) @@ -73,7 +65,7 @@ class TellMessagesMgrTest { assertThat(testMessages, "testMessages").size().isEqualTo(size - 1) } - @Test(groups = ["commands", "tell"]) + @Test fun loadTest() { val messages = TellManager.load(testFile.toAbsolutePath().toString()) for (i in messages.indices) { @@ -84,4 +76,14 @@ class TellMessagesMgrTest { } } } + + companion object { + private val testFile = createTempFile(suffix = ".ser") + + @JvmStatic + @AfterClass + fun afterClass() { + testFile.deleteIfExists() + } + } } diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/entries/EntriesUtilsTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/entries/EntriesUtilsTest.kt index 6eef16e..8e9bd6f 100644 --- a/src/test/kotlin/net/thauvin/erik/mobibot/entries/EntriesUtilsTest.kt +++ b/src/test/kotlin/net/thauvin/erik/mobibot/entries/EntriesUtilsTest.kt @@ -1,7 +1,7 @@ /* * EntriesUtilsTest.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -39,7 +39,7 @@ import net.thauvin.erik.mobibot.entries.EntriesUtils.printComment import net.thauvin.erik.mobibot.entries.EntriesUtils.printLink import net.thauvin.erik.mobibot.entries.EntriesUtils.printTags import net.thauvin.erik.mobibot.entries.EntriesUtils.toLinkLabel -import org.testng.annotations.Test +import kotlin.test.Test class EntriesUtilsTest { private val comment = EntryComment("comment", "nick") @@ -58,12 +58,12 @@ class EntriesUtilsTest { } } - @Test(groups = ["entries"]) + @Test fun printCommentTest() { assertThat(printComment(0, 0, comment)).isEqualTo("${Constants.LINK_CMD}1.1: [nick] comment") } - @Test(groups = ["entries"]) + @Test fun printLinkTest() { for (i in links.indices) { assertThat( @@ -75,7 +75,7 @@ class EntriesUtilsTest { assertThat(printLink(0, links.first(), isView = true), "printLink(isView=true)").contains("[+1]") } - @Test(groups = ["entries"]) + @Test fun printTagsTest() { for (i in links.indices) { assertThat( @@ -84,7 +84,7 @@ class EntriesUtilsTest { } } - @Test(groups = ["entries"]) + @Test fun toLinkLabelTest() { assertThat(1.toLinkLabel()).isEqualTo("${Constants.LINK_CMD}2") } diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/entries/EntryLinkTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/entries/EntryLinkTest.kt index ab3feee..495ed15 100644 --- a/src/test/kotlin/net/thauvin/erik/mobibot/entries/EntryLinkTest.kt +++ b/src/test/kotlin/net/thauvin/erik/mobibot/entries/EntryLinkTest.kt @@ -1,7 +1,7 @@ /* * EntryLinkTest.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -35,9 +35,9 @@ import assertk.assertThat import assertk.assertions.* 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.* +import kotlin.test.Test /** * The `EntryUtilsTest` class. @@ -52,7 +52,7 @@ class EntryLinkTest { listOf("tag1", "tag2", "tag3", "TAG4", "Tag5") ) - @Test(groups = ["entries"]) + @Test fun testAddDeleteComment() { var i = 0 while (i < 5) { @@ -85,7 +85,7 @@ class EntryLinkTest { assertThat(entryLink.deleteComment(comment), "comment is already deleted").isFalse() } - @Test(groups = ["entries"]) + @Test fun testConstructor() { val tags = listOf(SyndCategoryImpl().apply { name = "tag1" }, SyndCategoryImpl().apply { name = "tag2" }) val link = EntryLink("link", "title", "nick", "channel", Date(), tags) @@ -95,7 +95,7 @@ class EntryLinkTest { } } - @Test(groups = ["entries"]) + @Test fun testMatches() { assertThat(entryLink.matches("mobitopia"), "matches(mobitopia)").isTrue() assertThat(entryLink.matches("skynx"), "match(nick)").isTrue() @@ -106,7 +106,7 @@ class EntryLinkTest { } - @Test(groups = ["entries"]) + @Test fun testTags() { val tags: List = entryLink.tags for ((i, tag) in tags.withIndex()) { diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/entries/FeedMgrTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/entries/FeedMgrTest.kt index 4223d9d..de6075d 100644 --- a/src/test/kotlin/net/thauvin/erik/mobibot/entries/FeedMgrTest.kt +++ b/src/test/kotlin/net/thauvin/erik/mobibot/entries/FeedMgrTest.kt @@ -1,7 +1,7 @@ /* * FeedMgrTest.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -35,27 +35,25 @@ import assertk.all import assertk.assertThat import assertk.assertions.* 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.* import kotlin.io.path.deleteIfExists import kotlin.io.path.fileSize import kotlin.io.path.name +import kotlin.test.Test class FeedMgrTest { private val entries = Entries() private val channel = "mobibot" - @BeforeSuite(alwaysRun = true) - fun beforeSuite() { + init { entries.logsDir = "src/test/resources/" entries.ircServer = "irc.example.com" entries.channel = channel entries.backlogs = "https://www.mobitopia.org/mobibot/logs" } - @Test(groups = ["entries"]) + @Test fun testFeedMgr() { // Load the feed assertThat(FeedsManager.loadFeed(entries), "loadFeed()").isEqualTo("2021-10-31") diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/modules/CalcTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/modules/CalcTest.kt index 2b1d3f9..6df5616 100644 --- a/src/test/kotlin/net/thauvin/erik/mobibot/modules/CalcTest.kt +++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/CalcTest.kt @@ -1,7 +1,7 @@ /* * CalcTest.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -37,13 +37,13 @@ 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 +import kotlin.test.Test /** * The `CalcTest` class. */ class CalcTest { - @Test(groups = ["modules"]) + @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()}") diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/modules/ChatGptTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/modules/ChatGptTest.kt index 66fb98d..00fa43f 100644 --- a/src/test/kotlin/net/thauvin/erik/mobibot/modules/ChatGptTest.kt +++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/ChatGptTest.kt @@ -1,7 +1,7 @@ /* * ChatGptTest.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -35,18 +35,20 @@ import assertk.assertThat import assertk.assertions.contains import assertk.assertions.hasNoCause import assertk.assertions.isInstanceOf +import net.thauvin.erik.mobibot.DisabledOnCi import net.thauvin.erik.mobibot.LocalProperties -import org.testng.annotations.Test +import kotlin.test.Test class ChatGptTest : LocalProperties() { - @Test(groups = ["modules"]) + @Test fun testApiKey() { assertFailure { ChatGpt.chat("1 gallon to liter", "", 0) } .isInstanceOf(ModuleException::class.java) .hasNoCause() } - @Test(groups = ["modules", "no-ci"]) + @Test + @DisabledOnCi fun testChat() { val apiKey = getProperty(ChatGpt.API_KEY_PROP) assertThat( diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/modules/CryptoPricesTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/modules/CryptoPricesTest.kt index 0547c95..9847080 100644 --- a/src/test/kotlin/net/thauvin/erik/mobibot/modules/CryptoPricesTest.kt +++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/CryptoPricesTest.kt @@ -1,7 +1,7 @@ /* * CryptoPricesTest.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -39,20 +39,17 @@ import net.thauvin.erik.crypto.CryptoPrice import net.thauvin.erik.mobibot.modules.CryptoPrices.Companion.currentPrice import net.thauvin.erik.mobibot.modules.CryptoPrices.Companion.getCurrencyName import net.thauvin.erik.mobibot.modules.CryptoPrices.Companion.loadCurrencies -import org.testng.annotations.BeforeClass -import org.testng.annotations.Test +import kotlin.test.Test /** * The `CryptoPricesTest` class. */ class CryptoPricesTest { - @BeforeClass - @Throws(ModuleException::class) - fun before() { + init { loadCurrencies() } - @Test(groups = ["modules"]) + @Test @Throws(ModuleException::class) fun testMarketPrice() { var price = currentPrice(listOf("BTC")) @@ -70,7 +67,7 @@ class CryptoPricesTest { } } - @Test(groups = ["modules"]) + @Test fun testGetCurrencyName() { assertThat(getCurrencyName("USD"), "USD").isEqualTo("United States Dollar") assertThat(getCurrencyName("EUR"), "EUR").isEqualTo("Euro") diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/modules/CurrencyConverterTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/modules/CurrencyConverterTest.kt index 5375784..57affe5 100644 --- a/src/test/kotlin/net/thauvin/erik/mobibot/modules/CurrencyConverterTest.kt +++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/CurrencyConverterTest.kt @@ -1,7 +1,7 @@ /* * CurrencyConverterTest.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -42,23 +42,19 @@ import net.thauvin.erik.mobibot.modules.CurrencyConverter.Companion.loadSymbols 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 +import kotlin.test.Test /** * The `CurrencyConvertTest` class. */ class CurrencyConverterTest : LocalProperties() { - - @BeforeClass - @Throws(ModuleException::class) - fun before() { + init { val apiKey = getProperty(CurrencyConverter.API_KEY_PROP) loadSymbols(apiKey) } - @Test(groups = ["modules"]) + @Test fun testConvertCurrency() { val apiKey = getProperty(CurrencyConverter.API_KEY_PROP) assertThat( diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/modules/DiceTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/modules/DiceTest.kt index cdc04f0..0aaacb9 100644 --- a/src/test/kotlin/net/thauvin/erik/mobibot/modules/DiceTest.kt +++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/DiceTest.kt @@ -1,7 +1,7 @@ /* * DiceTest.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -35,10 +35,10 @@ package net.thauvin.erik.mobibot.modules import assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.matches -import org.testng.annotations.Test +import kotlin.test.Test class DiceTest { - @Test(groups = ["modules"]) + @Test fun testRoll() { assertThat(Dice.roll(1, 1), "roll(1d1)").isEqualTo("\u00021\u0002") assertThat(Dice.roll(2, 1), "roll(2d1)") diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/modules/GoogleSearchTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/modules/GoogleSearchTest.kt index fa50f8c..b0c154a 100644 --- a/src/test/kotlin/net/thauvin/erik/mobibot/modules/GoogleSearchTest.kt +++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/GoogleSearchTest.kt @@ -1,7 +1,7 @@ /* * GoogleSearchTest.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -34,18 +34,19 @@ import assertk.all import assertk.assertFailure import assertk.assertThat import assertk.assertions.* +import net.thauvin.erik.mobibot.DisabledOnCi 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 +import kotlin.test.Test /** * The `GoogleSearchTest` class. */ class GoogleSearchTest : LocalProperties() { - @Test(groups = ["modules"]) + @Test fun testAPIKeys() { assertThat( searchGoogle("", "apikey", "cssKey").first(), @@ -63,7 +64,8 @@ class GoogleSearchTest : LocalProperties() { .hasMessage("API key not valid. Please pass a valid API key.") } - @Test(groups = ["no-ci", "modules"]) + @Test + @DisabledOnCi @Throws(ModuleException::class) fun testSearchGoogle() { val apiKey = getProperty(GoogleSearch.API_KEY_PROP) diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/modules/JokeTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/modules/JokeTest.kt index 55a7b8f..9b8dcb3 100644 --- a/src/test/kotlin/net/thauvin/erik/mobibot/modules/JokeTest.kt +++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/JokeTest.kt @@ -1,7 +1,7 @@ /* * JokeTest.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -36,13 +36,13 @@ import assertk.assertions.* import net.thauvin.erik.mobibot.modules.Joke.Companion.randomJoke import net.thauvin.erik.mobibot.msg.Message import net.thauvin.erik.mobibot.msg.PublicMessage -import org.testng.annotations.Test +import kotlin.test.Test /** * The `JokeTest` class. */ class JokeTest { - @Test(groups = ["modules"]) + @Test @Throws(ModuleException::class) fun testRandomJoke() { val joke = randomJoke() diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/modules/LookupTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/modules/LookupTest.kt index 9c21f7c..e67c8ad 100644 --- a/src/test/kotlin/net/thauvin/erik/mobibot/modules/LookupTest.kt +++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/LookupTest.kt @@ -1,7 +1,7 @@ /* * LookupTest.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -35,13 +35,13 @@ 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 +import kotlin.test.Test /** * The `Lookup Test` class. */ class LookupTest { - @Test(groups = ["modules"]) + @Test @Throws(Exception::class) fun testLookup() { var result = nslookup("apple.com") @@ -51,7 +51,7 @@ class LookupTest { assertThat(result, "lookup(204.122.16.136)").contains("nix3.thauvin.us") } - @Test(groups = ["modules"]) + @Test @Throws(Exception::class) fun testWhois() { val result = whois("17.178.96.59", Lookup.WHOIS_HOST) diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/modules/MastodonTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/modules/MastodonTest.kt index 34f778a..d189d75 100644 --- a/src/test/kotlin/net/thauvin/erik/mobibot/modules/MastodonTest.kt +++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/MastodonTest.kt @@ -1,7 +1,7 @@ /* * MastodonTest.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -34,10 +34,10 @@ import assertk.assertThat import assertk.assertions.contains import net.thauvin.erik.mobibot.LocalProperties import net.thauvin.erik.mobibot.modules.Mastodon.Companion.toot -import org.testng.annotations.Test +import kotlin.test.Test class MastodonTest : LocalProperties() { - @Test(groups = ["modules"]) + @Test @Throws(ModuleException::class) fun testToot() { val msg = "Testing Mastodon API from ${getHostName()}" diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/modules/ModuleExceptionTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/modules/ModuleExceptionTest.kt index db68280..9dda5b3 100644 --- a/src/test/kotlin/net/thauvin/erik/mobibot/modules/ModuleExceptionTest.kt +++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/ModuleExceptionTest.kt @@ -1,7 +1,7 @@ /* * ModuleExceptionTest.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -34,10 +34,10 @@ import assertk.all import assertk.assertThat import assertk.assertions.* import net.thauvin.erik.mobibot.ExceptionSanitizer.sanitize -import org.testng.annotations.DataProvider -import org.testng.annotations.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.MethodSource import java.io.IOException -import java.lang.reflect.Method +import kotlin.test.Test /** * The `ModuleExceptionTest` class. @@ -46,28 +46,30 @@ class ModuleExceptionTest { companion object { const val DEBUG_MESSAGE = "debugMessage" const val MESSAGE = "message" + + @JvmStatic + fun dataProviders(): List { + return listOf( + ModuleException(DEBUG_MESSAGE, MESSAGE, IOException("URL http://foobar.com")), + ModuleException(DEBUG_MESSAGE, MESSAGE, IOException("URL http://foobar.com?")), + ModuleException(DEBUG_MESSAGE, MESSAGE) + ) + } } - @DataProvider(name = "dp") - fun createData(@Suppress("UNUSED_PARAMETER") m: Method?): Array> { - return arrayOf( - arrayOf(ModuleException(DEBUG_MESSAGE, MESSAGE, IOException("URL http://foobar.com"))), - arrayOf(ModuleException(DEBUG_MESSAGE, MESSAGE, IOException("URL http://foobar.com?"))), - arrayOf(ModuleException(DEBUG_MESSAGE, MESSAGE)) - ) - } - - @Test(dataProvider = "dp") + @ParameterizedTest + @MethodSource("dataProviders") fun testGetDebugMessage(e: ModuleException) { assertThat(e::debugMessage).isEqualTo(DEBUG_MESSAGE) } - @Test(dataProvider = "dp") + @ParameterizedTest + @MethodSource("dataProviders") fun testGetMessage(e: ModuleException) { assertThat(e).hasMessage(MESSAGE) } - @Test(groups = ["modules"]) + @Test fun testSanitizeMessage() { val apiKey = "1234567890" var e = ModuleException(DEBUG_MESSAGE, MESSAGE, IOException("URL http://foo.com?apiKey=$apiKey&userID=me")) diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/modules/PingTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/modules/PingTest.kt index e1e79f3..a6ff707 100644 --- a/src/test/kotlin/net/thauvin/erik/mobibot/modules/PingTest.kt +++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/PingTest.kt @@ -1,7 +1,7 @@ /* * PingTest.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -34,18 +34,18 @@ 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 +import kotlin.test.Test /** * The `PingTest` class. */ class PingTest { - @Test(groups = ["modules"]) + @Test fun testPingsArray() { assertThat(Ping.PINGS, "Ping.PINGS").isNotEmpty() } - @Test(groups = ["modules"]) + @Test fun testRandomPing() { for (i in 0..9) { assertThat(Ping.PINGS, "Ping.PINGS[$i]").contains(randomPing()) diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/modules/RockPaperScissorsTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/modules/RockPaperScissorsTest.kt index 8dc90ba..fb8865b 100644 --- a/src/test/kotlin/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 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -34,10 +34,10 @@ package net.thauvin.erik.mobibot.modules import assertk.assertThat import assertk.assertions.isEqualTo import net.thauvin.erik.mobibot.modules.RockPaperScissors.Companion.winLoseOrDraw -import org.testng.annotations.Test +import kotlin.test.Test class RockPaperScissorsTest { - @Test(groups = ["modules"]) + @Test fun testWinLoseOrDraw() { assertThat(winLoseOrDraw("scissors", "paper"), "scissors vs. paper").isEqualTo("win") assertThat(winLoseOrDraw("paper", "rock"), "paper vs. rock").isEqualTo("win") diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/modules/StockQuoteTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/modules/StockQuoteTest.kt index aff4188..ae8cff2 100644 --- a/src/test/kotlin/net/thauvin/erik/mobibot/modules/StockQuoteTest.kt +++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/StockQuoteTest.kt @@ -1,7 +1,7 @@ /* * StockQuoteTest.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -39,7 +39,7 @@ 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 +import kotlin.test.Test /** * The `StockQuoteTest` class. @@ -49,7 +49,7 @@ class StockQuoteTest : LocalProperties() { return "${label}:[ ]+[0-9.]+".prependIndent() } - @Test(groups = ["modules"]) + @Test @Throws(ModuleException::class) fun testGetQuote() { val apiKey = getProperty(StockQuote.API_KEY_PROP) diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/modules/Weather2Test.kt b/src/test/kotlin/net/thauvin/erik/mobibot/modules/Weather2Test.kt index d7d65de..e135866 100644 --- a/src/test/kotlin/net/thauvin/erik/mobibot/modules/Weather2Test.kt +++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/Weather2Test.kt @@ -1,7 +1,7 @@ /* * Weather2Test.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -43,19 +43,19 @@ 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 net.thauvin.erik.mobibot.msg.Message -import org.testng.annotations.Test +import kotlin.test.Test /** * The `Weather2Test` class. */ class Weather2Test : LocalProperties() { - @Test(groups = ["modules"]) + @Test fun testFtoC() { val t = ftoC(32.0) assertThat(t.second, "32 °F is 0 °C").isEqualTo(0) } - @Test(groups = ["modules"]) + @Test fun testGetCountry() { assertThat(getCountry("foo"), "foo is not a valid country").isEqualTo(OWM.Country.UNITED_STATES) assertThat(getCountry("fr"), "country should France").isEqualTo(OWM.Country.FRANCE) @@ -67,13 +67,13 @@ class Weather2Test : LocalProperties() { } } - @Test(groups = ["modules"]) + @Test fun testMphToKmh() { val w = mphToKmh(0.62) assertThat(w.second, "0.62 mph is 1 km/h").isEqualTo(1) } - @Test(groups = ["modules"]) + @Test @Throws(ModuleException::class) fun testWeather() { var query = "98204" diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/modules/WolframAlphaTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/modules/WolframAlphaTest.kt index ae1722d..417f9fc 100644 --- a/src/test/kotlin/net/thauvin/erik/mobibot/modules/WolframAlphaTest.kt +++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/WolframAlphaTest.kt @@ -1,7 +1,7 @@ /* * WolframAlphaTest.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -36,13 +36,14 @@ import assertk.assertThat import assertk.assertions.contains import assertk.assertions.hasMessage import assertk.assertions.isInstanceOf +import net.thauvin.erik.mobibot.DisabledOnCi import net.thauvin.erik.mobibot.ExceptionSanitizer.sanitize import net.thauvin.erik.mobibot.LocalProperties import net.thauvin.erik.mobibot.modules.WolframAlpha.Companion.queryWolfram -import org.testng.annotations.Test +import kotlin.test.Test class WolframAlphaTest : LocalProperties() { - @Test(groups = ["modules"]) + @Test fun testAppId() { assertFailure { queryWolfram("1 gallon to liter", appId = "DEMO") } .isInstanceOf(ModuleException::class.java) @@ -52,7 +53,8 @@ class WolframAlphaTest : LocalProperties() { .isInstanceOf(ModuleException::class.java) } - @Test(groups = ["modules", "no-ci"]) + @Test + @DisabledOnCi @Throws(ModuleException::class) fun queryWolframTest() { val apiKey = getProperty(WolframAlpha.APPID_KEY_PROP) diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/modules/WordTimeTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/modules/WordTimeTest.kt index 3602a27..5c8cc84 100644 --- a/src/test/kotlin/net/thauvin/erik/mobibot/modules/WordTimeTest.kt +++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/WordTimeTest.kt @@ -1,7 +1,7 @@ /* * WordTimeTest.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -39,14 +39,14 @@ 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 +import kotlin.test.Test /** * The `WordTimeTest` class. */ class WordTimeTest { - @Test(groups = ["modules"]) + @Test fun testTime() { assertThat(time(), "time()").matches( ("The time is ${Colors.BOLD}\\d{1,2}:\\d{2}${Colors.BOLD} " + @@ -61,7 +61,7 @@ class WordTimeTest { assertThat(time("BEAT"), "time($BEATS_KEYWORD)").matches("[\\w ]+ .?@\\d{3}+.? .beats".toRegex()) } - @Test(groups = ["modules"]) + @Test fun testZones() { COUNTRIES_MAP.filter { it.value != BEATS_KEYWORD }.forEach { assertThat(ZoneId.of(it.value)) diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/msg/MessageTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/msg/MessageTest.kt index e856112..477c1a4 100644 --- a/src/test/kotlin/net/thauvin/erik/mobibot/msg/MessageTest.kt +++ b/src/test/kotlin/net/thauvin/erik/mobibot/msg/MessageTest.kt @@ -1,7 +1,7 @@ /* * MessageTest.kt * - * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * Copyright 2021-2023 Erik C. Thauvin (erik@thauvin.net) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -36,7 +36,7 @@ import assertk.assertThat import assertk.assertions.isFalse import assertk.assertions.isTrue import assertk.assertions.prop -import org.testng.annotations.Test +import kotlin.test.Test class MessageTest { @Test diff --git a/src/test/resources/current.xml b/src/test/resources/current.xml index 535a400..8552a9a 100644 --- a/src/test/resources/current.xml +++ b/src/test/resources/current.xml @@ -1,35 +1,4 @@ - - #mobibot IRC Links diff --git a/version.mustache b/version.mustache deleted file mode 100644 index b2672e4..0000000 --- a/version.mustache +++ /dev/null @@ -1,32 +0,0 @@ -/* -* This file is automatically generated. -* Do not modify! -- ALL CHANGES WILL BE ERASED! -*/ -package {{packageName}} - -import java.time.LocalDateTime -import java.time.ZoneId -import java.time.Instant - -/** -* 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()) - - 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 deleted file mode 100644 index 9c446af..0000000 --- a/version.properties +++ /dev/null @@ -1,9 +0,0 @@ -#Generated by the Semver Plugin for Gradle -#Wed Nov 01 22:09:32 PDT 2023 -version.buildmeta=20231101220932 -version.major=0 -version.minor=8 -version.patch=0 -version.prerelease=rc -version.project=mobibot -version.semver=0.8.0-rc+20231101220932