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 0000000..6c36e54 Binary files /dev/null and b/bin/main/net/thauvin/erik/mobibot/modules/War.class differ 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 < '} - 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 0000000..d17ad89 Binary files /dev/null and b/lib/bld/bld-wrapper.jar differ 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