diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..c781fdc --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,62 @@ +version: 2.1 + +orbs: + sdkman: joshdholtz/sdkman@0.2.0 + +defaults: &defaults + working_directory: ~/repo + environment: + JVM_OPTS: -Xmx3200m + TERM: dumb + CI_NAME: "CircleCI" + +commands: + build_and_test: + parameters: + reports-dir: + type: string + default: "build/reports/test_results" + steps: + - checkout + - sdkman/setup-sdkman + - sdkman/sdkman-install: + candidate: kotlin + version: 2.1.10 + - run: + name: Download dependencies + command: ./bld download + - run: + name: Compile source + command: ./bld compile + - run: + name: Run tests + command: ./bld jacoco -reports-dir=<< parameters.reports-dir >> + - store_test_results: + path: << parameters.reports-dir >> + - store_artifacts: + path: build/reports/jacoco/test/html + +jobs: + bld_jdk17: + <<: *defaults + + docker: + - image: cimg/openjdk:17.0 + + steps: + - build_and_test + + bld_jdk21: + <<: *defaults + + docker: + - image: cimg/openjdk:21.0 + + steps: + - build_and_test + +workflows: + bld: + jobs: + - bld_jdk17 + - bld_jdk21 diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..1f808de --- /dev/null +++ b/.editorconfig @@ -0,0 +1,2 @@ +[*] +insert_final_newline = true diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6ec2ae2 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +# Set the default behavior, in case people don't have core.autocrlf set. +* text=auto + +# batch files are specific to windows and always crlf +*.bat eol=crlf diff --git a/.github/workflows/bld.yml b/.github/workflows/bld.yml new file mode 100644 index 0000000..8e7d939 --- /dev/null +++ b/.github/workflows/bld.yml @@ -0,0 +1,58 @@ +name: bld-ci + +on: [push, pull_request, workflow_dispatch] + +env: + ALPHAVANTAGE_API_KEY: ${{ secrets.ALPHAVANTAGE_API_KEY }} + CHATGPT_API_KEY: ${{ secrets.CHATGPT_API_KEY }} + CI_NAME: "GitHub CI" + COVERAGE_JDK: "21" + COVERAGE_KOTLIN: "2.1.0" + EXCHANGERATE_API_KEY: ${{ secrets.EXCHANGERATE_API_KEY }} + KOTLIN_HOME: /usr/share/kotlinc + MASTODON_ACCESS_TOKEN: ${{ secrets.MASTODON_ACCESS_TOKEN }} + MASTODON_HANDLE: ${{ secrets.MASTODON_HANDLE }} + MASTODON_INSTANCE: ${{ secrets.MASTODON_INSTANCE }} + OWM_API_KEY: ${{ secrets.OWM_API_KEY }} + PINBOARD_API_TOKEN: ${{ secrets.PINBOARD_API_TOKEN }} + +jobs: + build-bld-project: + runs-on: ubuntu-latest + + strategy: + matrix: + java-version: [17, 21, 24] + kotlin-version: [1.9.25, 2.0.20, 2.1.20] + + steps: + - name: Checkout source repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up JDK ${{ matrix.java-version }} with Kotlin ${{ matrix.kotlin-version }} + uses: actions/setup-java@v4 + with: + distribution: "zulu" + java-version: ${{ matrix.java-version }} + + - name: Download dependencies + run: ./bld download + + - name: Compile source + run: ./bld compile + + - name: Run tests + run: ./bld jacoco + + - name: Remove pom.xml + if: success() && matrix.java-version == env.COVERAGE_JDK && matrix.kotlin-version == env.COVERAGE_KOTLIN + run: rm -rf pom.xml + + - name: SonarCloud Scan + uses: sonarsource/sonarcloud-github-action@master + if: success() && matrix.java-version == env.COVERAGE_JDK && matrix.kotlin-version == env.COVERAGE_KOTLIN + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d2daf1c --- /dev/null +++ b/.gitignore @@ -0,0 +1,63 @@ +.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 + +bin +deploy +local.properties +logs +mobibot.properties +out +/target/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..10b9b0f --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,24 @@ +image: fedora:latest + +variables: + CI_NAME: "GitLab CI" + +stages: + - test + +before_script: + - dnf -qy update && dnf -y install zip + - curl -s "https://get.sdkman.io" | bash + - echo sdkman_auto_answer=true > $HOME/.sdkman/etc/config + - echo sdkman_auto_selfupdate=true >> $HOME/.sdkman/etc/config + - source "$HOME/.sdkman/bin/sdkman-init.sh" + - sdk install java + - sdk install kotlin + - source "$HOME/.sdkman/bin/sdkman-init.sh" + +test: + stage: test + 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/bld.xml b/.idea/bld.xml new file mode 100644 index 0000000..6600cee --- /dev/null +++ b/.idea/bld.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/copyright/BSD_3.xml b/.idea/copyright/BSD_3.xml new file mode 100644 index 0000000..3c57002 --- /dev/null +++ b/.idea/copyright/BSD_3.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml new file mode 100644 index 0000000..3203074 --- /dev/null +++ b/.idea/copyright/profiles_settings.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..1e01b48 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/.idea/intellij-javadocs-4.0.1.xml b/.idea/intellij-javadocs-4.0.1.xml new file mode 100644 index 0000000..dd24abe --- /dev/null +++ b/.idea/intellij-javadocs-4.0.1.xml @@ -0,0 +1,204 @@ + + + + + UPDATE + false + true + + FIELD + METHOD + TYPE + + + PROTECTED + DEFAULT + PUBLIC + + + + + + ^.*(public|protected|private)*.+interface\s+\w+.* + /**\n + * The interface ${name}.\n +<#if element.typeParameters?has_content> * \n +</#if> +<#list element.typeParameters as typeParameter> + * @param <${typeParameter.name}> the type parameter\n +</#list> + */ + + + ^.*(public|protected|private)*.+enum\s+\w+.* + /**\n + * The enum ${name}.\n + */ + + + ^.*(public|protected|private)*.+class\s+\w+.* + /**\n + * The type ${name}.\n +<#if element.typeParameters?has_content> * \n +</#if> +<#list element.typeParameters as typeParameter> + * @param <${typeParameter.name}> the type parameter\n +</#list> + */ + + + .+ + /**\n + * The type ${name}.\n + */ + + + + + .+ + /**\n + * Instantiates a new ${name}.\n +<#if element.parameterList.parameters?has_content> + *\n +</#if> +<#list element.parameterList.parameters as parameter> + * @param ${parameter.name} the ${paramNames[parameter.name]}\n +</#list> +<#if element.throwsList.referenceElements?has_content> + *\n +</#if> +<#list element.throwsList.referenceElements as exception> + * @throws ${exception.referenceName} the ${exceptionNames[exception.referenceName]}\n +</#list> + */ + + + + + ^.*(public|protected|private)*\s*.*(\w(\s*<.+>)*)+\s+get\w+\s*\(.*\).+ + /**\n + * Gets ${partName}.\n +<#if element.typeParameters?has_content> * \n +</#if> +<#list element.typeParameters as typeParameter> + * @param <${typeParameter.name}> the type parameter\n +</#list> +<#if element.parameterList.parameters?has_content> + *\n +</#if> +<#list element.parameterList.parameters as parameter> + * @param ${parameter.name} the ${paramNames[parameter.name]}\n +</#list> +<#if isNotVoid> + *\n + * @return the ${partName}\n +</#if> +<#if element.throwsList.referenceElements?has_content> + *\n +</#if> +<#list element.throwsList.referenceElements as exception> + * @throws ${exception.referenceName} the ${exceptionNames[exception.referenceName]}\n +</#list> + */ + + + ^.*(public|protected|private)*\s*.*(void|\w(\s*<.+>)*)+\s+set\w+\s*\(.*\).+ + /**\n + * Sets ${partName}.\n +<#if element.typeParameters?has_content> * \n +</#if> +<#list element.typeParameters as typeParameter> + * @param <${typeParameter.name}> the type parameter\n +</#list> +<#if element.parameterList.parameters?has_content> + *\n +</#if> +<#list element.parameterList.parameters as parameter> + * @param ${parameter.name} the ${paramNames[parameter.name]}\n +</#list> +<#if isNotVoid> + *\n + * @return the ${partName}\n +</#if> +<#if element.throwsList.referenceElements?has_content> + *\n +</#if> +<#list element.throwsList.referenceElements as exception> + * @throws ${exception.referenceName} the ${exceptionNames[exception.referenceName]}\n +</#list> + */ + + + ^.*((public\s+static)|(static\s+public))\s+void\s+main\s*\(\s*String\s*(\[\s*\]|\.\.\.)\s+\w+\s*\).+ + /**\n + * The entry point of application.\n + + <#if element.parameterList.parameters?has_content> + *\n +</#if> + * @param ${element.parameterList.parameters[0].name} the input arguments\n +<#if element.throwsList.referenceElements?has_content> + *\n +</#if> +<#list element.throwsList.referenceElements as exception> + * @throws ${exception.referenceName} the ${exceptionNames[exception.referenceName]}\n +</#list> + */ + + + .+ + /**\n + * ${name}<#if isNotVoid> ${return}</#if>.\n +<#if element.typeParameters?has_content> * \n +</#if> +<#list element.typeParameters as typeParameter> + * @param <${typeParameter.name}> the type parameter\n +</#list> +<#if element.parameterList.parameters?has_content> + *\n +</#if> +<#list element.parameterList.parameters as parameter> + * @param ${parameter.name} the ${paramNames[parameter.name]}\n +</#list> +<#if isNotVoid> + *\n + * @return the ${return}\n +</#if> +<#if element.throwsList.referenceElements?has_content> + *\n +</#if> +<#list element.throwsList.referenceElements as exception> + * @throws ${exception.referenceName} the ${exceptionNames[exception.referenceName]}\n +</#list> + */ + + + + + ^.*(public|protected|private)*.+static.*(\w\s\w)+.+ + /**\n + * The constant ${element.getName()}.\n + */ + + + ^.*(public|protected|private)*.*(\w\s\w)+.+ + /**\n + <#if element.parent.isInterface()> + * The constant ${element.getName()}.\n +<#else> + * The ${name}.\n +</#if> */ + + + .+ + /**\n + <#if element.parent.isEnum()> + *${name} ${typeName}.\n +<#else> + * The ${name}.\n +</#if>*/ + + + + + \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml new file mode 100644 index 0000000..0273acf --- /dev/null +++ b/.idea/kotlinc.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/bld.xml b/.idea/libraries/bld.xml new file mode 100644 index 0000000..153a060 --- /dev/null +++ b/.idea/libraries/bld.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/.idea/libraries/compile.xml b/.idea/libraries/compile.xml new file mode 100644 index 0000000..99cc0c0 --- /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..d4069f2 --- /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..57ed5ef --- /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 new file mode 100644 index 0000000..bccad37 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + 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/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..7b016a8 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "java.compile.nullAnalysis.mode": "automatic" +} \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..54e8774 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,26 @@ +Copyright 2004-2025 Erik C. Thauvin (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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..d43ba67 --- /dev/null +++ b/README.md @@ -0,0 +1,33 @@ +# 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-2.1.20-7f52ff.svg)](https://kotlinlang.org) +[![bld](https://img.shields.io/badge/2.2.1-FA9052?label=bld&labelColor=2392FF)](https://rife2.com/bld) +[![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/bld.yml/badge.svg)](https://github.com/ethauvin/mobibot/actions/workflows/bld.yml) +[![CircleCI](https://circleci.com/gh/ethauvin/mobibot/tree/master.svg?style=shield)](https://circleci.com/gh/ethauvin/mobibot/tree/master) + +Some very basic instructions: + +```sh + # clone with git or download the ZIP + git clone https://github.com/ethauvin/mobibot.git + + cd mobibot + + # build JAR and deploy + ./bld jar deploy + + cd deploy + + # configure the properties + vi *.properties *.xml + + # help + java -jar mobibot.jar -h + + # launch + /usr/bin/nohup java -jar mobibot.jar & +``` + +For a listing of features, see the [website](https://mobitopia.org/mobibot/). diff --git a/bitbucket-pipelines.yml b/bitbucket-pipelines.yml new file mode 100644 index 0000000..ace99d2 --- /dev/null +++ b/bitbucket-pipelines.yml @@ -0,0 +1,20 @@ +image: ubuntu:latest + +pipelines: + default: + - step: + name: Test with bld + script: + # Install latest Java & Kotlin via SDKMAN! + - apt-get update -qq && apt-get install -y curl zip + - curl -s "https://get.sdkman.io" | bash + - echo sdkman_auto_answer=true > $HOME/.sdkman/etc/config + - echo sdkman_auto_selfupdate=true >> $HOME/.sdkman/etc/config + - source "$HOME/.sdkman/bin/sdkman-init.sh" + - sdk install java + - sdk install kotlin + - source "$HOME/.sdkman/bin/sdkman-init.sh" + # Download, compile and test with bld + - ./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/buildnum.properties b/buildnum.properties deleted file mode 100644 index e332b08..0000000 --- a/buildnum.properties +++ /dev/null @@ -1,3 +0,0 @@ -#ANT Task: ch.oscg.jreleaseinfo.BuildNumberHandler -#Tue Sep 14 16:51:11 PDT 2010 -build.num.last=8 diff --git a/config/detekt/baseline.xml b/config/detekt/baseline.xml new file mode 100644 index 0000000..719ca4e --- /dev/null +++ b/config/detekt/baseline.xml @@ -0,0 +1,112 @@ + + + + + CyclomaticComplexMethod:FeedsManager.kt$FeedsManager.Companion$@JvmStatic fun saveFeed(entries: Entries, currentFile: String = CURRENT_XML) + CyclomaticComplexMethod:Weather2.kt$Weather2.Companion$@JvmStatic @Throws(ModuleException::class) fun getWeather(query: String, apiKey: String?): List<Message> + LongMethod:FeedsManager.kt$FeedsManager.Companion$@JvmStatic fun saveFeed(entries: Entries, currentFile: String = CURRENT_XML) + LongMethod:Mobibot.kt$Mobibot.Companion$@JvmStatic @Throws(Exception::class) fun main(args: Array<String>) + LongMethod:StockQuote.kt$StockQuote.Companion$@JvmStatic @Throws(ModuleException::class) fun getQuote(symbol: String, apiKey: String?): List<Message> + LongMethod:Weather2.kt$Weather2.Companion$@JvmStatic @Throws(ModuleException::class) fun getWeather(query: String, apiKey: String?): List<Message> + LongParameterList:Comment.kt$Comment$( channel: String, cmd: String, entry: EntryLink, entryIndex: Int, commentIndex: Int, event: GenericMessageEvent ) + LongParameterList:EntryLink.kt$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 ) + MagicNumber:ChatGpt.kt$ChatGpt.Companion$200 + MagicNumber:ChatGpt.kt$ChatGpt.Companion$429 + MagicNumber:Comment.kt$Comment$3 + MagicNumber:CryptoPrices.kt$CryptoPrices$10 + MagicNumber:CurrencyConverter.kt$CurrencyConverter$11 + MagicNumber:CurrencyConverter.kt$CurrencyConverter.Companion$3 + MagicNumber:CurrencyConverter.kt$CurrencyConverter.Companion$4 + MagicNumber:Cycle.kt$Cycle$10 + MagicNumber:Cycle.kt$Cycle$1000L + MagicNumber:Ignore.kt$Ignore$8 + MagicNumber:Info.kt$Info.Companion$30 + MagicNumber:Info.kt$Info.Companion$365 + MagicNumber:Info.kt$Info.Companion$7 + MagicNumber:Mastodon.kt$Mastodon.Companion$200 + MagicNumber:Mobibot.kt$Mobibot$8 + MagicNumber:Modules.kt$Modules$7 + MagicNumber:Seen.kt$Seen$7 + MagicNumber:SocialManager.kt$SocialManager$1000L + MagicNumber:SocialManager.kt$SocialManager$60L + MagicNumber:StockQuote.kt$StockQuote.Companion$10 + MagicNumber:Tell.kt$Tell$50 + MagicNumber:Tell.kt$Tell$7 + MagicNumber:Users.kt$Users$8 + MagicNumber:Utils.kt$Utils$200 + MagicNumber:Utils.kt$Utils$399 + MagicNumber:Weather2.kt$Weather2.Companion$1.60934 + MagicNumber:Weather2.kt$Weather2.Companion$32 + MagicNumber:Weather2.kt$Weather2.Companion$404 + MagicNumber:Weather2.kt$Weather2.Companion$5 + MagicNumber:Weather2.kt$Weather2.Companion$9 + MagicNumber:WorldTime.kt$WorldTime$14 + MagicNumber:WorldTime.kt$WorldTime$4 + MagicNumber:WorldTime.kt$WorldTime.Companion$3600 + MagicNumber:WorldTime.kt$WorldTime.Companion$60 + MagicNumber:WorldTime.kt$WorldTime.Companion$86.4 + NestedBlockDepth:Addons.kt$Addons$fun add(command: AbstractCommand): Boolean + NestedBlockDepth:Addons.kt$Addons$fun add(module: AbstractModule): Boolean + NestedBlockDepth:ChatGpt.kt$ChatGpt.Companion$@JvmStatic @Throws(ModuleException::class) fun chat(query: String, apiKey: String?, maxTokens: Int): String + NestedBlockDepth:Comment.kt$Comment$override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) + NestedBlockDepth:CurrencyConverter.kt$CurrencyConverter.Companion$@JvmStatic @Throws(ModuleException::class) fun loadSymbols(apiKey: String?) + NestedBlockDepth:CurrencyConverter.kt$CurrencyConverter.Companion$@JvmStatic fun convertCurrency(apiKey: String?, query: String): Message + NestedBlockDepth:EntryLink.kt$EntryLink$private fun setTags(tags: List<String?>) + NestedBlockDepth:FeedsManager.kt$FeedsManager.Companion$@JvmStatic @Throws(IOException::class, FeedException::class) fun loadFeed(entries: Entries, currentFile: String = CURRENT_XML): String + NestedBlockDepth:FeedsManager.kt$FeedsManager.Companion$@JvmStatic fun saveFeed(entries: Entries, currentFile: String = CURRENT_XML) + NestedBlockDepth:GoogleSearch.kt$GoogleSearch$override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) + NestedBlockDepth:GoogleSearch.kt$GoogleSearch.Companion$@JvmStatic @Throws(ModuleException::class) fun searchGoogle( query: String, apiKey: String?, cseKey: String?, quotaUser: String = ReleaseInfo.PROJECT ): List<Message> + NestedBlockDepth:LinksManager.kt$LinksManager$override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) + NestedBlockDepth:Lookup.kt$Lookup$override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) + NestedBlockDepth:Mastodon.kt$Mastodon.Companion$@JvmStatic @Throws(ModuleException::class) fun toot(apiKey: String?, instance: String?, handle: String?, message: String, isDm: Boolean): String + NestedBlockDepth:Posting.kt$Posting$override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) + NestedBlockDepth:Seen.kt$Seen$override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) + NestedBlockDepth:StockQuote.kt$StockQuote.Companion$@JvmStatic @Throws(ModuleException::class) fun getQuote(symbol: String, apiKey: String?): List<Message> + NestedBlockDepth:Tell.kt$Tell$fun send(event: GenericUserEvent) + NestedBlockDepth:Utils.kt$Utils$@JvmStatic fun loadSerialData(file: String, default: Any, logger: Logger, description: String): Any + NestedBlockDepth:Utils.kt$Utils$@JvmStatic fun saveSerialData(file: String, data: Any, logger: Logger, description: String) + NestedBlockDepth:Weather2.kt$Weather2$override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) + NestedBlockDepth:Weather2.kt$Weather2.Companion$@JvmStatic @Throws(ModuleException::class) fun getWeather(query: String, apiKey: String?): List<Message> + ReturnCount:Addons.kt$Addons$fun exec(channel: String, cmd: String, args: String, event: GenericMessageEvent): Boolean + ReturnCount:Addons.kt$Addons$fun help(channel: String, topic: String, event: GenericMessageEvent): Boolean + ReturnCount:ExceptionSanitizer.kt$ExceptionSanitizer$fun ModuleException.sanitize(vararg sanitize: String): ModuleException + ReturnCount:Seen.kt$Seen$override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) + ThrowsCount:ChatGpt.kt$ChatGpt.Companion$@JvmStatic @Throws(ModuleException::class) fun chat(query: String, apiKey: String?, maxTokens: Int): String + ThrowsCount:GoogleSearch.kt$GoogleSearch.Companion$@JvmStatic @Throws(ModuleException::class) fun searchGoogle( query: String, apiKey: String?, cseKey: String?, quotaUser: String = ReleaseInfo.PROJECT ): List<Message> + ThrowsCount:Joke.kt$Joke.Companion$@JvmStatic @Throws(ModuleException::class) fun randomJoke(): List<Message> + ThrowsCount:Mastodon.kt$Mastodon.Companion$@JvmStatic @Throws(ModuleException::class) fun toot(apiKey: String?, instance: String?, handle: String?, message: String, isDm: Boolean): String + ThrowsCount:StockQuote.kt$StockQuote.Companion$@JvmStatic @Throws(ModuleException::class) fun getQuote(symbol: String, apiKey: String?): List<Message> + ThrowsCount:StockQuote.kt$StockQuote.Companion$@Throws(ModuleException::class) private fun getJsonResponse(response: String, debugMessage: String): JSONObject + ThrowsCount:Weather2.kt$Weather2.Companion$@JvmStatic @Throws(ModuleException::class) fun getWeather(query: String, apiKey: String?): List<Message> + ThrowsCount:WolframAlpha.kt$WolframAlpha.Companion$@JvmStatic @Throws(ModuleException::class) fun queryWolfram(query: String, units: String = IMPERIAL, appId: String?): String + TooGenericExceptionCaught:ChatGpt2.kt$ChatGpt2.Companion$e: Exception + TooGenericExceptionCaught:Gemini.kt$Gemini.Companion$e: Exception + TooGenericExceptionCaught:Gemini2.kt$Gemini2.Companion$e: Exception + TooGenericExceptionCaught:StockQuote.kt$StockQuote.Companion$e: NullPointerException + TooGenericExceptionCaught:Weather2.kt$Weather2.Companion$e: NullPointerException + TooManyFunctions:EntryLink.kt$EntryLink : Serializable + TooManyFunctions:Mobibot.kt$Mobibot : ListenerAdapter + TooManyFunctions:Tell.kt$Tell : AbstractCommand + UtilityClassWithPublicConstructor:LocalProperties.kt$LocalProperties + WildcardImport:AddonsTest.kt$import net.thauvin.erik.mobibot.modules.* + WildcardImport:EntryLinkTest.kt$import assertk.assertions.* + WildcardImport:FeedMgrTest.kt$import assertk.assertions.* + WildcardImport:FeedReaderTest.kt$import assertk.assertions.* + WildcardImport:FeedsManager.kt$import com.rometools.rome.feed.synd.* + WildcardImport:Gemini2Test.kt$import assertk.assertions.* + WildcardImport:GeminiTest.kt$import assertk.assertions.* + WildcardImport:GoogleSearchTest.kt$import assertk.assertions.* + WildcardImport:JokeTest.kt$import assertk.assertions.* + WildcardImport:Mobibot.kt$import java.io.* + WildcardImport:Mobibot.kt$import net.thauvin.erik.mobibot.commands.* + WildcardImport:Mobibot.kt$import net.thauvin.erik.mobibot.commands.links.* + WildcardImport:Mobibot.kt$import net.thauvin.erik.mobibot.modules.* + WildcardImport:Mobibot.kt$import org.pircbotx.hooks.events.* + WildcardImport:ModuleExceptionTest.kt$import assertk.assertions.* + WildcardImport:SeenTest.kt$import assertk.assertions.* + WildcardImport:StockQuoteTest.kt$import assertk.assertions.* + WildcardImport:TellMessagesMgrTest.kt$import assertk.assertions.* + WildcardImport:Utils.kt$import java.io.* + WildcardImport:Weather2Test.kt$import assertk.assertions.* + + diff --git a/deploy.fish b/deploy.fish new file mode 100755 index 0000000..d69ee4e --- /dev/null +++ b/deploy.fish @@ -0,0 +1,11 @@ +#!/usr/bin/env fish + +./bld clean jar deploy +if test $status -eq 0 + echo "cd /home/mobibot/mobitopia/mobibot +lcd deploy +put *.jar +cd lib +rm *.jar +put lib/*.jar" | sftp nix4 +end diff --git a/lib/bld/bld-wrapper.jar b/lib/bld/bld-wrapper.jar new file mode 100644 index 0000000..73cde27 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..8b96558 --- /dev/null +++ b/lib/bld/bld-wrapper.properties @@ -0,0 +1,10 @@ +bld.downloadExtensionJavadoc=false +bld.downloadExtensionSources=true +bld.downloadLocation= +bld.extension-detekt=com.uwyn.rife2:bld-detekt:0.9.10-SNAPSHOT +bld.extension-gv=com.uwyn.rife2:bld-generated-version:1.0.1 +bld.extension-jacoco=com.uwyn.rife2:bld-jacoco-report:0.9.10 +bld.extension-kotlin=com.uwyn.rife2:bld-kotlin:1.1.0-SNAPSHOT +bld.repositories=MAVEN_LOCAL,MAVEN_CENTRAL,RIFE2_SNAPSHOTS,RIFE2_RELEASES +bld.sourceDirectories= +bld.version=2.2.1 diff --git a/lib/commons-cli-1.2.jar b/lib/commons-cli-1.2.jar deleted file mode 100644 index ce4b9ff..0000000 Binary files a/lib/commons-cli-1.2.jar and /dev/null differ diff --git a/lib/commons-codec-1.4.jar b/lib/commons-codec-1.4.jar deleted file mode 100644 index 458d432..0000000 Binary files a/lib/commons-codec-1.4.jar and /dev/null differ diff --git a/lib/log4j-1.2.16.jar b/lib/log4j-1.2.16.jar deleted file mode 100644 index 3f9d847..0000000 Binary files a/lib/log4j-1.2.16.jar and /dev/null differ diff --git a/lib/xmlrpc-client-3.1.3.jar b/lib/xmlrpc-client-3.1.3.jar deleted file mode 100644 index 38e3359..0000000 Binary files a/lib/xmlrpc-client-3.1.3.jar and /dev/null differ diff --git a/lib/xmlrpc-common-3.1.3.jar b/lib/xmlrpc-common-3.1.3.jar deleted file mode 100644 index b5eb9df..0000000 Binary files a/lib/xmlrpc-common-3.1.3.jar and /dev/null differ diff --git a/licenses/Apache License.txt b/licenses/Apache License.txt new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/licenses/Apache License.txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/licenses/JDOM License.txt b/licenses/JDOM License.txt new file mode 100644 index 0000000..660cedb --- /dev/null +++ b/licenses/JDOM License.txt @@ -0,0 +1,53 @@ +/*-- + + Copyright (C) 2000-2012 Jason Hunter & Brett McLaughlin. + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions, and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions, and the disclaimer that follows + these conditions in the documentation and/or other materials + provided with the distribution. + + 3. The name "JDOM" must not be used to endorse or promote products + derived from this software without prior written permission. For + written permission, please contact . + + 4. Products derived from this software may not be called "JDOM", nor + may "JDOM" appear in their name, without prior written permission + from the JDOM Project Management . + + In addition, we request (but do not require) that you include in the + end-user documentation provided with the redistribution and/or in the + software itself an acknowledgement equivalent to the following: + "This product includes software developed by the + JDOM Project (http://www.jdom.org/)." + Alternatively, the acknowledgment may be graphical using the logos + available at http://www.jdom.org/images/logos. + + THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED + WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE JDOM AUTHORS OR THE PROJECT + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF + USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + SUCH DAMAGE. + + This software consists of voluntary contributions made by many + individuals on behalf of the JDOM Project and was originally + created by Jason Hunter and + Brett McLaughlin . For more information + on the JDOM Project, please see . + + */ diff --git a/licenses/JSON License.txt b/licenses/JSON License.txt new file mode 100644 index 0000000..4933197 --- /dev/null +++ b/licenses/JSON License.txt @@ -0,0 +1,21 @@ +Copyright (c) 2002 JSON.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/licenses/LICENSE.txt b/licenses/LICENSE.txt new file mode 100644 index 0000000..085f7c7 --- /dev/null +++ b/licenses/LICENSE.txt @@ -0,0 +1,27 @@ +Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net) +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of this project nor the names of its contributors may be + used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/licenses/OWM JAPIs License.txt b/licenses/OWM JAPIs License.txt new file mode 100644 index 0000000..cf80d7d --- /dev/null +++ b/licenses/OWM JAPIs License.txt @@ -0,0 +1,19 @@ +Copyright (c) 2013- Ashutosh Kumar Singh + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/licenses/PircBotX.txt b/licenses/PircBotX.txt new file mode 100644 index 0000000..9fa1ca4 --- /dev/null +++ b/licenses/PircBotX.txt @@ -0,0 +1,13 @@ +Copyright (C) 2010-2014 Leon Blakey + +PircBotX is free software: you can redistribute it and/or modify it under the +terms of the GNU General Public License as published by the Free Software +Foundation, either version 3 of the License, or (at your option) any later +version. + +PircBotX is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +A PARTICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with +PircBotX. If not, see . \ No newline at end of file diff --git a/licenses/SLF4J License.txt b/licenses/SLF4J License.txt new file mode 100644 index 0000000..744377c --- /dev/null +++ b/licenses/SLF4J License.txt @@ -0,0 +1,21 @@ +Copyright (c) 2004-2017 QOS.ch +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/licenses/jsoup License.txt b/licenses/jsoup License.txt new file mode 100644 index 0000000..31b785d --- /dev/null +++ b/licenses/jsoup License.txt @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) 2009-2022 Jonathan Hedley + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/mobibot.iml b/mobibot.iml deleted file mode 100644 index c589342..0000000 --- a/mobibot.iml +++ /dev/null @@ -1,202 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/mobibot.iws b/mobibot.iws deleted file mode 100644 index dd94c42..0000000 --- a/mobibot.iws +++ /dev/null @@ -1,1183 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Inspections - - - - - Inspections - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1267745358497 - 1267745358497 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..58768ef --- /dev/null +++ b/pom.xml @@ -0,0 +1,199 @@ + + + 4.0.0 + net.thauvin.erik.mobibot + mobibot + 0.8.0-rc+20250424113056 + mobibot + + + + + com.github.pircbotx + pircbotx + 2.3.1 + compile + + + org.apache.commons + commons-lang3 + 3.17.0 + compile + + + org.apache.commons + commons-text + 1.13.1 + compile + + + commons-codec + commons-codec + 1.18.0 + compile + + + commons-net + commons-net + 3.11.1 + compile + + + com.google.code.gson + gson + 2.13.1 + compile + + + com.google.guava + guava + 33.2.1-jre + compile + + + org.jetbrains.kotlin + kotlin-stdlib + 2.1.20 + compile + + + org.jetbrains.kotlin + kotlin-stdlib-common + 2.1.20 + compile + + + org.jetbrains.kotlin + kotlin-stdlib-jdk7 + 2.1.20 + compile + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + 2.1.20 + compile + + + org.jetbrains.kotlinx + kotlinx-coroutines-core + 1.10.2 + compile + + + org.jetbrains.kotlinx + kotlinx-cli-jvm + 0.3.6 + compile + + + org.slf4j + slf4j-api + 2.0.17 + compile + + + org.apache.logging.log4j + log4j-api + 2.24.3 + compile + + + org.apache.logging.log4j + log4j-core + 2.24.3 + compile + + + org.apache.logging.log4j + log4j-slf4j2-impl + 2.24.3 + compile + + + dev.langchain4j + langchain4j-open-ai + 0.36.2 + compile + + + dev.langchain4j + langchain4j-google-ai-gemini + 0.36.2 + compile + + + dev.langchain4j + langchain4j-core + 0.36.2 + compile + + + dev.langchain4j + langchain4j + 0.36.2 + compile + + + com.rometools + rome + 2.1.0 + compile + + + com.squareup.okhttp3 + okhttp + 4.12.0 + compile + + + net.aksingh + owm-japis + 2.5.3.0 + compile + + + net.objecthunter + exp4j + 0.4.8 + compile + + + org.json + json + 20250107 + compile + + + org.jsoup + jsoup + 1.19.1 + compile + + + net.thauvin.erik + cryptoprice + 1.0.3-SNAPSHOT + compile + + + net.thauvin.erik + jokeapi + 1.0.1-SNAPSHOT + compile + + + net.thauvin.erik + pinboard-poster + 1.2.1-SNAPSHOT + compile + + + net.thauvin.erik.urlencoder + urlencoder-lib-jvm + 1.6.0 + compile + + + diff --git a/properties/log4j2.xml b/properties/log4j2.xml new file mode 100644 index 0000000..0aa95bf --- /dev/null +++ b/properties/log4j2.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/properties/mobibot.properties b/properties/mobibot.properties new file mode 100644 index 0000000..fd7b104 --- /dev/null +++ b/properties/mobibot.properties @@ -0,0 +1,86 @@ +channel=#mobitopia +server=irc.libera.chat +#port=6667 +login=mobibot +nick=mobibot +#realname=mobibot + +# Die command password, if any +#die=changeme + +# NickServ password +ident=changeme +#ident-nick=nickserv +#ident-msg=IDENTIFY changepwd + +logs=./logs +ignore=chanserv,nickserv +tags=mobile mobitopia +tags-keywords=android ios apple google + +feed=http://www.mobitopia.org/rss.xml +backlogs=http://www.mobitopia.org/mobibot/logs + +tell-max-days=5 +tell-max-size=50 + +#disabled-commands=die, ignore +disabled-modules=mastodon + +# +# API Token for: https://pinboard.in/settings/password +# +#pinboard-api-token=user\:TOKEN + +# +# Create a Mastodon application access token at: https//SERVER_INSTANCE/settings/applications +# Make sure the 'write:statuses' scope is enabled. +# +#mastodon-access-token= +#mastodon-instance=mastodon.social + +# Mastodon handle to receive channel join/leave notifications +#mastodon-handle=@mobitopia + +# Automatically post links to Mastodon +#mastodon-auto-post=true + +# +# Get Exchange Rate API key from: https://www.exchangerate-api.com/ +# +#exchangerate-api-key= + +# +# Create custom search engine at: https://programmablesearchengine.google.com/ +# and get API key from: https://console.cloud.google.com/apis +# +#google-api= +#google-cse-cx= + +# +# Get OpenWeatherMap API key from: https://openweathermap.org/api +# +#owm-api-key= + +# +# Get Alpha Vantage Stock Quote API key from: https://www.alphavantage.co/support/#api-key +# +#alphavantage-api-key= + +# +# Get Wolfram Alpha AppID from: https://developer.wolframalpha.com/portal/ +# +#wolfram-appid= +#wolfram-units=imperial + +# +# Get ChatGPT/OpenAI API key from: https://platform.openai.com/api-keys +# +#chatgpt-api-key= +#chatgpt-max-tokens=1024 + +# +# Get Google Gemini API key from https://ai.google.dev/gemini-api/docs/api-key +# +#gemini-api-key= +#gemini-max-tokens=1024 diff --git a/release_info.txt b/release_info.txt new file mode 100644 index 0000000..d2c33ac --- /dev/null +++ b/release_info.txt @@ -0,0 +1,28 @@ +/* + * 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 + @Suppress("MagicNumber") + val BUILD_DATE: LocalDateTime = LocalDateTime.ofInstant( + Instant.ofEpochMilli({{v epoch/}}L), ZoneId.systemDefault() + ) + + const val WEBSITE = "https://mobitopia.org/mobibot/" + const val AUTHOR = "Erik C. Thauvin" + const val AUTHOR_URL = "https://erik.thauvin.net/" +} 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..08367ef --- /dev/null +++ b/src/bld/java/net/thauvin/erik/MobibotBuild.java @@ -0,0 +1,238 @@ +/* + * MobibotBuild.java + * + * Copyright 2004-2025 Erik C. Thauvin (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.DetektOperation; +import rife.bld.extension.GeneratedVersionOperation; +import rife.bld.extension.JacocoReportOperation; +import rife.bld.extension.kotlin.CompileOptions; +import rife.bld.operations.exceptions.ExitStatusException; +import rife.bld.publish.PomBuilder; +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 java.util.logging.ConsoleHandler; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static rife.bld.dependencies.Repository.*; +import static rife.bld.dependencies.Scope.compile; +import static rife.bld.dependencies.Scope.test; + +public class MobibotBuild extends Project { + private static final String DETEKT_BASELINE = "config/detekt/baseline.xml"; + final File srcMainKotlin = new File(srcMainDirectory(), "kotlin"); + + 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"), + SONATYPE_SNAPSHOTS_LEGACY); + + var log4j = version(2, 24, 3); + var kotlin = version(2, 1, 20); + var langchain = version(0, 36, 2); + scope(compile) + // PircBotX + .include(dependency("com.github.pircbotx", "pircbotx", "2.3.1")) + // Commons (mostly for PircBotX) + .include(dependency("org.apache.commons", "commons-lang3", "3.17.0")) + .include(dependency("org.apache.commons", "commons-text", "1.13.1")) + .include(dependency("commons-codec", "commons-codec", "1.18.0")) + .include(dependency("commons-net", "commons-net", "3.11.1")) + // Google + .include(dependency("com.google.code.gson", "gson", "2.13.1")) + .include(dependency("com.google.guava", "guava", "33.2.1-jre")) + // Kotlin + .include(dependency("org.jetbrains.kotlin", "kotlin-stdlib", kotlin)) + .include(dependency("org.jetbrains.kotlin", "kotlin-stdlib-common", kotlin)) + .include(dependency("org.jetbrains.kotlin", "kotlin-stdlib-jdk7", kotlin)) + .include(dependency("org.jetbrains.kotlin", "kotlin-stdlib-jdk8", kotlin)) + .include(dependency("org.jetbrains.kotlinx", "kotlinx-coroutines-core", "1.10.2")) + .include(dependency("org.jetbrains.kotlinx", "kotlinx-cli-jvm", "0.3.6")) + // Logging + .include(dependency("org.slf4j", "slf4j-api", "2.0.17")) + .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)) + // LangChain4J + .include(dependency("dev.langchain4j", "langchain4j-open-ai", langchain)) + .include(dependency("dev.langchain4j", "langchain4j-google-ai-gemini", langchain)) + .include(dependency("dev.langchain4j", "langchain4j-core", langchain)) + .include(dependency("dev.langchain4j", "langchain4j", langchain)) + // Misc. + .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", "20250107")) + .include(dependency("org.jsoup", "jsoup", "1.19.1")) + // Thauvin + .include(dependency("net.thauvin.erik", "cryptoprice", "1.0.3-SNAPSHOT")) + .include(dependency("net.thauvin.erik", "jokeapi", "1.0.1-SNAPSHOT")) + .include(dependency("net.thauvin.erik", "pinboard-poster", "1.2.1-SNAPSHOT")) + .include(dependency("net.thauvin.erik.urlencoder", "urlencoder-lib-jvm", "1.6.0")); + scope(test) + .include(dependency("com.willowtreeapps.assertk", "assertk-jvm", version(0, 28, 1))) + .include(dependency("org.jetbrains.kotlin", "kotlin-test-junit5", kotlin)) + .include(dependency("org.junit.jupiter", "junit-jupiter", version(5, 12, 2))) + .include(dependency("org.junit.platform", "junit-platform-console-standalone", version(1, 12, 2))) + .include(dependency("org.junit.platform", "junit-platform-launcher", version(1, 12, 2))); + + List jars = new ArrayList<>(); + runtimeClasspathJars().forEach(f -> jars.add("./lib/" + f.getName())); + 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(srcMainKotlin); + } + + public static void main(String[] args) { + var level = Level.ALL; + var logger = Logger.getLogger("rife.bld.extension"); + var consoleHandler = new ConsoleHandler(); + + consoleHandler.setLevel(level); + logger.addHandler(consoleHandler); + logger.setLevel(level); + logger.setUseParentHandlers(false); + + new MobibotBuild().start(args); + } + + @Override + public void clean() throws Exception { + var deploy = new File("deploy"); + if (deploy.exists()) { + FileUtils.deleteDirectory(deploy); + } + super.clean(); + } + + @BuildCommand(summary = "Compiles the Kotlin project") + @Override + public void compile() throws Exception { + releaseInfo(); + new CompileKotlinOperation() + .compileOptions(new CompileOptions().progressive(true).verbose(true)) + .fromProject(this) + .execute(); + } + + @Override + public void updates() throws Exception { + super.updates(); + pomRoot(); + } + + @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); + for (var jar : compileClasspathJars()) { + FileUtils.copy(jar, new File(lib, jar.getName())); + } + for (var jar : runtimeClasspathJars()) { + FileUtils.copy(jar, new File(lib, jar.getName())); + } + FileUtils.copy(new File(buildDistDirectory(), jarFileName()), new File(deploy, "mobibot.jar")); + } + + @BuildCommand(summary = "Checks source with Detekt") + public void detekt() throws ExitStatusException, IOException, InterruptedException { + new DetektOperation() + .fromProject(this) + .baseline(DETEKT_BASELINE) + .execute(); + } + + @BuildCommand(value = "detekt-baseline", summary = "Creates the Detekt baseline") + public void detektBaseline() throws ExitStatusException, IOException, InterruptedException { + new DetektOperation() + .fromProject(this) + .baseline(DETEKT_BASELINE) + .createBaseline(true) + .execute(); + } + + @BuildCommand(summary = "Generates JaCoCo Reports") + public void jacoco() throws Exception { + new JacocoReportOperation() + .fromProject(this) + .sourceFiles(srcMainKotlin) + .execute(); + } + + @BuildCommand(value = "pom-root", summary = "Generates the POM file in the root directory") + public void pomRoot() throws FileUtilsErrorException { + PomBuilder.generateInto(publishOperation().fromProject(this).info(), dependencies(), + new File(workDirectory, "pom.xml")); + } + + @BuildCommand(value = "release-info", summary = "Generates the ReleaseInfo class") + public void releaseInfo() throws Exception { + new GeneratedVersionOperation() + .fromProject(this) + .classTemplate(new File(workDirectory(), "release-info.txt")) + .className("ReleaseInfo") + .packageName(pkg) + .directory(srcMainKotlin) + .extension(".kt") + .execute(); + } +} diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/Addons.kt b/src/main/kotlin/net/thauvin/erik/mobibot/Addons.kt new file mode 100644 index 0000000..ed34897 --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/mobibot/Addons.kt @@ -0,0 +1,190 @@ +/* + * Addons.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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/src/main/kotlin/net/thauvin/erik/mobibot/Constants.kt b/src/main/kotlin/net/thauvin/erik/mobibot/Constants.kt new file mode 100644 index 0000000..0dea8d5 --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/mobibot/Constants.kt @@ -0,0 +1,101 @@ +/* + * Constants.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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; rv:136.0) Gecko/20100101 Firefox/136.0" + + /** + * 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/src/main/kotlin/net/thauvin/erik/mobibot/FeedReader.kt b/src/main/kotlin/net/thauvin/erik/mobibot/FeedReader.kt new file mode 100644 index 0000000..3ab8d1f --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/mobibot/FeedReader.kt @@ -0,0 +1,92 @@ +/* + * FeedReader.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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/src/main/kotlin/net/thauvin/erik/mobibot/Mobibot.kt b/src/main/kotlin/net/thauvin/erik/mobibot/Mobibot.kt new file mode 100644 index 0000000..691a339 --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/mobibot/Mobibot.kt @@ -0,0 +1,420 @@ +/* + * Mobibot.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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 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 + +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.BUILD_DATE.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(ChatGpt2()) + addons.add(CryptoPrices()) + addons.add(CurrencyConverter()) + addons.add(Dice()) + addons.add(Gemini2()) + 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/src/main/kotlin/net/thauvin/erik/mobibot/Pinboard.kt b/src/main/kotlin/net/thauvin/erik/mobibot/Pinboard.kt new file mode 100644 index 0000000..f9076c9 --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/mobibot/Pinboard.kt @@ -0,0 +1,113 @@ +/* + * Pinboard.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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/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..42a61aa --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/mobibot/ReleaseInfo.kt @@ -0,0 +1,28 @@ +/* + * 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+20250322004101" + + @JvmField + @Suppress("MagicNumber") + val BUILD_DATE: LocalDateTime = LocalDateTime.ofInstant( + Instant.ofEpochMilli(1742629261438L), ZoneId.systemDefault() + ) + + const val WEBSITE = "https://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 new file mode 100644 index 0000000..97797ef --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/mobibot/Utils.kt @@ -0,0 +1,447 @@ +/* + * Utils.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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 + try { + connection.setRequestProperty( + "User-Agent", + Constants.USER_AGENT + ) + return if (connection.responseCode.isHttpSuccess()) { + UrlReaderResponse( + connection.responseCode, + connection.inputStream.bufferedReader().use { it.readText() }) + } else { + UrlReaderResponse( + connection.responseCode, + connection.errorStream.bufferedReader().use { it.readText() }) + } + } finally { + connection.disconnect() + } + } + + /** + * Holds the [URL.reader] response code and body text. + */ + data class UrlReaderResponse(val responseCode: Int, val body: String) +} diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/commands/AbstractCommand.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/AbstractCommand.kt new file mode 100644 index 0000000..4642f42 --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/AbstractCommand.kt @@ -0,0 +1,79 @@ +/* + * AbstractCommand.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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/src/main/kotlin/net/thauvin/erik/mobibot/commands/ChannelFeed.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/ChannelFeed.kt new file mode 100644 index 0000000..0075293 --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/ChannelFeed.kt @@ -0,0 +1,62 @@ +/* + * ChannelFeed.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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/src/main/kotlin/net/thauvin/erik/mobibot/commands/Cycle.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Cycle.kt new file mode 100644 index 0000000..cefcde3 --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Cycle.kt @@ -0,0 +1,66 @@ +/* + * Cycle.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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/src/main/kotlin/net/thauvin/erik/mobibot/commands/Die.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Die.kt new file mode 100644 index 0000000..d7577af --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Die.kt @@ -0,0 +1,62 @@ +/* + * Die.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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/src/main/kotlin/net/thauvin/erik/mobibot/commands/Ignore.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Ignore.kt new file mode 100644 index 0000000..13b20b0 --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Ignore.kt @@ -0,0 +1,146 @@ +/* + * Ignore.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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/src/main/kotlin/net/thauvin/erik/mobibot/commands/Info.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Info.kt new file mode 100644 index 0000000..8e244cc --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Info.kt @@ -0,0 +1,124 @@ +/* + * Info.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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/src/main/kotlin/net/thauvin/erik/mobibot/commands/Me.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Me.kt new file mode 100644 index 0000000..afa9046 --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Me.kt @@ -0,0 +1,51 @@ +/* + * Me.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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/src/main/kotlin/net/thauvin/erik/mobibot/commands/Modules.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Modules.kt new file mode 100644 index 0000000..8668bf7 --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Modules.kt @@ -0,0 +1,63 @@ +/* + * Modules.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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/src/main/kotlin/net/thauvin/erik/mobibot/commands/Msg.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Msg.kt new file mode 100644 index 0000000..14d8d8e --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Msg.kt @@ -0,0 +1,60 @@ +/* + * Msg.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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/src/main/kotlin/net/thauvin/erik/mobibot/commands/Nick.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Nick.kt new file mode 100644 index 0000000..21c96b5 --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Nick.kt @@ -0,0 +1,51 @@ +/* + * Nick.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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/src/main/kotlin/net/thauvin/erik/mobibot/commands/Recap.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Recap.kt new file mode 100644 index 0000000..500fd85 --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Recap.kt @@ -0,0 +1,81 @@ +/* + * Recap.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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/src/main/kotlin/net/thauvin/erik/mobibot/commands/Say.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Say.kt new file mode 100644 index 0000000..b9d410d --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Say.kt @@ -0,0 +1,51 @@ +/* + * Say.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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/src/main/kotlin/net/thauvin/erik/mobibot/commands/Users.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Users.kt new file mode 100644 index 0000000..960b8aa --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Users.kt @@ -0,0 +1,50 @@ +/* + * Users.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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/src/main/kotlin/net/thauvin/erik/mobibot/commands/Versions.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Versions.kt new file mode 100644 index 0000000..62cb044 --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Versions.kt @@ -0,0 +1,59 @@ +/* + * Versions.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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.BUILD_DATE.toIsoLocalDate()})", + "${System.getProperty("os.name")} ${System.getProperty("os.version")} (${System.getProperty("os.arch")})" + + ", JVM ${System.getProperty("java.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/src/main/kotlin/net/thauvin/erik/mobibot/commands/links/Comment.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/links/Comment.kt new file mode 100644 index 0000000..f0d9d0c --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/links/Comment.kt @@ -0,0 +1,151 @@ +/* + * Comment.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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/src/main/kotlin/net/thauvin/erik/mobibot/commands/links/LinksManager.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/links/LinksManager.kt new file mode 100644 index 0000000..e688092 --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/links/LinksManager.kt @@ -0,0 +1,207 @@ +/* + * LinksManager.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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/src/main/kotlin/net/thauvin/erik/mobibot/commands/links/Posting.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/links/Posting.kt new file mode 100644 index 0000000..a47021b --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/links/Posting.kt @@ -0,0 +1,164 @@ +/* + * Posting.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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/src/main/kotlin/net/thauvin/erik/mobibot/commands/links/Tags.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/links/Tags.kt new file mode 100644 index 0000000..0d73f6e --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/links/Tags.kt @@ -0,0 +1,87 @@ +/* + * Tags.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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/src/main/kotlin/net/thauvin/erik/mobibot/commands/links/View.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/links/View.kt new file mode 100644 index 0000000..6891c2d --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/links/View.kt @@ -0,0 +1,120 @@ +/* + * View.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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/src/main/kotlin/net/thauvin/erik/mobibot/commands/seen/NickComparator.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/seen/NickComparator.kt new file mode 100644 index 0000000..f44b357 --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/seen/NickComparator.kt @@ -0,0 +1,45 @@ +/* + * NickComparator.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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/src/main/kotlin/net/thauvin/erik/mobibot/commands/seen/Seen.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/seen/Seen.kt new file mode 100644 index 0000000..8af98dc --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/seen/Seen.kt @@ -0,0 +1,150 @@ +/* + * Seen.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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/src/main/kotlin/net/thauvin/erik/mobibot/commands/seen/SeenNick.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/seen/SeenNick.kt new file mode 100644 index 0000000..21d7cb9 --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/seen/SeenNick.kt @@ -0,0 +1,41 @@ +/* + * SeenNick.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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/src/main/kotlin/net/thauvin/erik/mobibot/commands/tell/Tell.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/tell/Tell.kt new file mode 100644 index 0000000..26fe803 --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/tell/Tell.kt @@ -0,0 +1,306 @@ +/* + * Tell.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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" + + // The 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/src/main/kotlin/net/thauvin/erik/mobibot/commands/tell/TellManager.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/tell/TellManager.kt new file mode 100644 index 0000000..f193a3c --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/tell/TellManager.kt @@ -0,0 +1,74 @@ +/* + * TellManager.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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/src/main/kotlin/net/thauvin/erik/mobibot/commands/tell/TellMessage.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/tell/TellMessage.kt new file mode 100644 index 0000000..1f55687 --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/tell/TellMessage.kt @@ -0,0 +1,104 @@ +/* + * TellMessage.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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/src/main/kotlin/net/thauvin/erik/mobibot/entries/Entries.kt b/src/main/kotlin/net/thauvin/erik/mobibot/entries/Entries.kt new file mode 100644 index 0000000..4e187d4 --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/mobibot/entries/Entries.kt @@ -0,0 +1,54 @@ +/* + * Entries.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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/src/main/kotlin/net/thauvin/erik/mobibot/entries/EntriesUtils.kt b/src/main/kotlin/net/thauvin/erik/mobibot/entries/EntriesUtils.kt new file mode 100644 index 0000000..1588704 --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/mobibot/entries/EntriesUtils.kt @@ -0,0 +1,83 @@ +/* + * EntriesUtils.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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/src/main/kotlin/net/thauvin/erik/mobibot/entries/EntryComment.kt b/src/main/kotlin/net/thauvin/erik/mobibot/entries/EntryComment.kt new file mode 100644 index 0000000..1826101 --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/mobibot/entries/EntryComment.kt @@ -0,0 +1,52 @@ +/* + * EntryComment.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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/src/main/kotlin/net/thauvin/erik/mobibot/entries/EntryLink.kt b/src/main/kotlin/net/thauvin/erik/mobibot/entries/EntryLink.kt new file mode 100644 index 0000000..a807f07 --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/mobibot/entries/EntryLink.kt @@ -0,0 +1,213 @@ +/* + * EntryLink.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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/src/main/kotlin/net/thauvin/erik/mobibot/entries/FeedsManager.kt b/src/main/kotlin/net/thauvin/erik/mobibot/entries/FeedsManager.kt new file mode 100644 index 0000000..a881204 --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/mobibot/entries/FeedsManager.kt @@ -0,0 +1,187 @@ +/* + * FeedsManager.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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/src/main/kotlin/net/thauvin/erik/mobibot/modules/AbstractModule.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/AbstractModule.kt new file mode 100644 index 0000000..1ced830 --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/AbstractModule.kt @@ -0,0 +1,131 @@ +/* + * AbstractModule.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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/src/main/kotlin/net/thauvin/erik/mobibot/modules/Calc.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/Calc.kt new file mode 100644 index 0000000..7fd320f --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/Calc.kt @@ -0,0 +1,87 @@ +/* + * Calc.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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/src/main/kotlin/net/thauvin/erik/mobibot/modules/ChatGpt2.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/ChatGpt2.kt new file mode 100644 index 0000000..fbf0e94 --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/ChatGpt2.kt @@ -0,0 +1,128 @@ +/* + * ChatGpt2.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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 dev.langchain4j.model.openai.OpenAiChatModel +import dev.langchain4j.model.openai.OpenAiChatModelName +import net.thauvin.erik.mobibot.Utils +import net.thauvin.erik.mobibot.Utils.sendMessage +import org.pircbotx.hooks.types.GenericMessageEvent +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +class ChatGpt2 : AbstractModule() { + val logger: Logger = LoggerFactory.getLogger(ChatGpt2::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(answer) + } 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 command + private const val CHATGPT_CMD = "chatgpt" + + @JvmStatic + @Throws(ModuleException::class) + fun chat(query: String, apiKey: String?, maxTokens: Int): String { + if (!apiKey.isNullOrEmpty()) { + try { + val model = OpenAiChatModel.builder() + .apiKey(apiKey) + .modelName(OpenAiChatModelName.GPT_4) + .maxTokens(maxTokens) + .build() + + return model.generate(query) + } catch (e: Exception) { + 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/src/main/kotlin/net/thauvin/erik/mobibot/modules/CryptoPrices.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/CryptoPrices.kt new file mode 100644 index 0000000..3334a90 --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/CryptoPrices.kt @@ -0,0 +1,159 @@ +/* + * CryptoPrices.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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.cdp.coinbase.com/coinbase-app/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/src/main/kotlin/net/thauvin/erik/mobibot/modules/CurrencyConverter.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/CurrencyConverter.kt new file mode 100644 index 0000000..2ff4715 --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/CurrencyConverter.kt @@ -0,0 +1,222 @@ +/* + * CurrencyConverter.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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[ChatGpt2.API_KEY_PROP]) + } +} diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/modules/Dice.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/Dice.kt new file mode 100644 index 0000000..5c1dd09 --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/Dice.kt @@ -0,0 +1,87 @@ +/* + * Dice.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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/src/main/kotlin/net/thauvin/erik/mobibot/modules/Gemini2.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/Gemini2.kt new file mode 100644 index 0000000..e85ea7b --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/Gemini2.kt @@ -0,0 +1,130 @@ +/* + * Gemini2.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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 dev.langchain4j.model.googleai.GoogleAiGeminiChatModel +import net.thauvin.erik.mobibot.Utils +import net.thauvin.erik.mobibot.Utils.sendMessage +import org.pircbotx.hooks.types.GenericMessageEvent +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.util.* + +class Gemini2 : AbstractModule() { + private val logger: Logger = LoggerFactory.getLogger(Gemini2::class.java) + + override val name = GEMINI_NAME + + override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) { + if (args.isNotBlank()) { + try { + val answer = chat( + args.trim(), + properties[GEMINI_API_KEY], + properties.getOrDefault(MAX_TOKENS_PROP, "1024").toInt() + ) + if (!answer.isNullOrEmpty()) { + event.sendMessage(answer) + } else { + event.respond("$name is stumped.") + } + } catch (e: ModuleException) { + if (logger.isWarnEnabled) logger.warn(e.debugMessage, e) + e.message?.let { + event.respond(it) + } + } + } else { + helpResponse(event) + } + } + + companion object { + /** + * The service name. + */ + const val GEMINI_NAME = "Gemini" + + /** + * The API key + */ + const val GEMINI_API_KEY = "gemini-api-key" + + /** + * The max number of output tokens property. + */ + const val MAX_TOKENS_PROP = "gemini-max-tokens" + + // Gemini command + private const val GEMINI_CMD = "gemini" + + @JvmStatic + @Throws(ModuleException::class) + fun chat( + query: String, + apiKey: String?, + maxTokens: Int + ): String? { + if (!apiKey.isNullOrEmpty()) { + try { + val gemini = GoogleAiGeminiChatModel.builder() + .apiKey(apiKey) + .modelName("gemini-2.0-flash") + .maxOutputTokens(maxTokens) + .build() + + return gemini.generate(query) + } catch (e: Exception) { + throw ModuleException( + "$GEMINI_CMD($query): IO", + "An IO error has occurred while conversing with ${GEMINI_NAME}.", + e + ) + } + } else { + throw ModuleException("${GEMINI_CMD}($query)", "No $GEMINI_NAME Project ID or Location specified.") + } + } + } + + init { + commands.add(GEMINI_CMD) + with(help) { + add("To get answers from $name:") + add(Utils.helpFormat("%c $GEMINI_CMD <query>")) + add("For example:") + add(Utils.helpFormat("%c $GEMINI_CMD explain quantum computing in simple terms")) + add(Utils.helpFormat("%c $GEMINI_CMD how do I make an HTTP request in Javascript?")) + } + initProperties(GEMINI_API_KEY, MAX_TOKENS_PROP) + } +} diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/modules/GoogleSearch.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/GoogleSearch.kt new file mode 100644 index 0000000..26f3e71 --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/GoogleSearch.kt @@ -0,0 +1,162 @@ +/* + * GoogleSearch.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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/src/main/kotlin/net/thauvin/erik/mobibot/modules/Joke.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/Joke.kt new file mode 100644 index 0000000..e792ed4 --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/Joke.kt @@ -0,0 +1,105 @@ +/* + * Joke.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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/src/main/kotlin/net/thauvin/erik/mobibot/modules/Lookup.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/Lookup.kt new file mode 100644 index 0000000..f700757 --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/Lookup.kt @@ -0,0 +1,171 @@ +/* + * Lookup.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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/src/main/kotlin/net/thauvin/erik/mobibot/modules/Mastodon.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/Mastodon.kt new file mode 100644 index 0000000..d4c2614 --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/Mastodon.kt @@ -0,0 +1,149 @@ +/* + * Mastodon.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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/src/main/kotlin/net/thauvin/erik/mobibot/modules/ModuleException.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/ModuleException.kt new file mode 100644 index 0000000..26d374a --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/ModuleException.kt @@ -0,0 +1,45 @@ +/* + * ModuleException.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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/src/main/kotlin/net/thauvin/erik/mobibot/modules/Ping.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/Ping.kt new file mode 100644 index 0000000..ca18216 --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/Ping.kt @@ -0,0 +1,83 @@ +/* + * Ping.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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/src/main/kotlin/net/thauvin/erik/mobibot/modules/RockPaperScissors.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/RockPaperScissors.kt new file mode 100644 index 0000000..b8c81f1 --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/RockPaperScissors.kt @@ -0,0 +1,114 @@ +/* + * RockPaperScissors.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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/src/main/kotlin/net/thauvin/erik/mobibot/modules/StockQuote.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/StockQuote.kt new file mode 100644 index 0000000..d71c91a --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/StockQuote.kt @@ -0,0 +1,236 @@ +/* + * StockQuote.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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/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..70ac4ec --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/War.kt @@ -0,0 +1,89 @@ +/* + * War.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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 new file mode 100644 index 0000000..074edd0 --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/Weather2.kt @@ -0,0 +1,250 @@ +/* + * Weather2.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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/src/main/kotlin/net/thauvin/erik/mobibot/modules/WolframAlpha.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/WolframAlpha.kt new file mode 100644 index 0000000..2e2e7ec --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/WolframAlpha.kt @@ -0,0 +1,142 @@ +/* + * WolframAlpha.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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/src/main/kotlin/net/thauvin/erik/mobibot/modules/WorldTime.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/WorldTime.kt new file mode 100644 index 0000000..afc0a5f --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/WorldTime.kt @@ -0,0 +1,390 @@ +/* + * WorldTime.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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/src/main/kotlin/net/thauvin/erik/mobibot/msg/ErrorMessage.kt b/src/main/kotlin/net/thauvin/erik/mobibot/msg/ErrorMessage.kt new file mode 100644 index 0000000..56e7b92 --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/mobibot/msg/ErrorMessage.kt @@ -0,0 +1,37 @@ +/* + * ErrorMessage.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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/src/main/kotlin/net/thauvin/erik/mobibot/msg/Message.kt b/src/main/kotlin/net/thauvin/erik/mobibot/msg/Message.kt new file mode 100644 index 0000000..1a6e58b --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/mobibot/msg/Message.kt @@ -0,0 +1,63 @@ +/* + * Message.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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 `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 = "" + } + + 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/src/main/kotlin/net/thauvin/erik/mobibot/msg/NoticeMessage.kt b/src/main/kotlin/net/thauvin/erik/mobibot/msg/NoticeMessage.kt new file mode 100644 index 0000000..f06ce89 --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/mobibot/msg/NoticeMessage.kt @@ -0,0 +1,38 @@ +/* + * NoticeMessage.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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/src/main/kotlin/net/thauvin/erik/mobibot/msg/PrivateMessage.kt b/src/main/kotlin/net/thauvin/erik/mobibot/msg/PrivateMessage.kt new file mode 100644 index 0000000..ef0eeb1 --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/mobibot/msg/PrivateMessage.kt @@ -0,0 +1,37 @@ +/* + * PrivateMessage.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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/src/main/kotlin/net/thauvin/erik/mobibot/msg/PublicMessage.kt b/src/main/kotlin/net/thauvin/erik/mobibot/msg/PublicMessage.kt new file mode 100644 index 0000000..be6583f --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/mobibot/msg/PublicMessage.kt @@ -0,0 +1,36 @@ +/* + * PublicMessage.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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/src/main/kotlin/net/thauvin/erik/mobibot/social/SocialManager.kt b/src/main/kotlin/net/thauvin/erik/mobibot/social/SocialManager.kt new file mode 100644 index 0000000..de9653d --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/mobibot/social/SocialManager.kt @@ -0,0 +1,116 @@ +/* + * SocialManager.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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/src/main/kotlin/net/thauvin/erik/mobibot/social/SocialModule.kt b/src/main/kotlin/net/thauvin/erik/mobibot/social/SocialModule.kt new file mode 100644 index 0000000..d45cf5c --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/mobibot/social/SocialModule.kt @@ -0,0 +1,96 @@ +/* + * SocialModule.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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/src/main/kotlin/net/thauvin/erik/mobibot/social/SocialTimer.kt b/src/main/kotlin/net/thauvin/erik/mobibot/social/SocialTimer.kt new file mode 100644 index 0000000..aadebf5 --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/mobibot/social/SocialTimer.kt @@ -0,0 +1,40 @@ +/* + * SocialTimer.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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/src/net/thauvin/erik/mobibot/ReleaseInfo.java b/src/net/thauvin/erik/mobibot/ReleaseInfo.java deleted file mode 100644 index 98080e5..0000000 --- a/src/net/thauvin/erik/mobibot/ReleaseInfo.java +++ /dev/null @@ -1,59 +0,0 @@ -/* Created by JReleaseInfo AntTask from Open Source Competence Group */ -/* Creation date Tue Sep 14 16:51:11 PDT 2010 */ -package net.thauvin.erik.mobibot; - -import java.util.Date; - -/** - * This class provides information gathered from the build environment. - * - * @author JReleaseInfo AntTask - */ -public class ReleaseInfo { - - /** - * Disables the default constructor. - * @throws UnsupportedOperationException if the constructor is called. - */ - private ReleaseInfo() throws UnsupportedOperationException { - throw new UnsupportedOperationException("Illegal constructor call."); - } - - - /** buildDate (set during build process to 1284508271605L). */ - private static final Date buildDate = new Date(1284508271605L); - - /** - * Get buildDate (set during build process to Tue Sep 14 16:51:11 PDT 2010). - * @return Date buildDate - */ - public static Date getBuildDate() { return buildDate; } - - - /** - * Get buildNumber (set during build process to 8). - * @return int buildNumber - */ - public static int getBuildNumber() { return 8; } - - - /** project (set during build process to "mobibot"). */ - private static final String project = "mobibot"; - - /** - * Get project (set during build process to "mobibot"). - * @return String project - */ - public static String getProject() { return project; } - - - /** version (set during build process to "0.4"). */ - private static final String version = "0.4"; - - /** - * Get version (set during build process to "0.4"). - * @return String version - */ - public static String getVersion() { return version; } - -} diff --git a/src/net/thauvin/erik/mobibot/TwitterOAuth.java b/src/net/thauvin/erik/mobibot/TwitterOAuth.java deleted file mode 100644 index d2f8c73..0000000 --- a/src/net/thauvin/erik/mobibot/TwitterOAuth.java +++ /dev/null @@ -1,81 +0,0 @@ -package net.thauvin.erik.mobibot; - -import twitter4j.TwitterException; -import twitter4j.TwitterFactory; -import twitter4j.http.AccessToken; -import twitter4j.http.RequestToken; - -import java.io.BufferedReader; -import java.io.InputStreamReader; - -/** - * The <code>TwitterOAuth</code> class. - * <p/> - * Go to <a href="http://twitter.com/oauth_clients/new">http://twitter.com/oauth_clients/new</a> to register your bot. - * Then execute: - * <p/> - * <code>java -cp "mobibot.jar:lib/*" net.thauvin.erik.mobibot.TwitterOAuth <consumerKey> <consumerSecret></code> - * <p/> - * and follow the prompts/instructions. - * - * @author <a href="mailto:erik@thauvin.net">Erik C. Thauvin</a> - * @author <a href="http://twitter4j.org/en/code-examples.html#oauth">http://twitter4j.org/en/code-examples.html#oauth</a> - * @version $Revision$, $Date$ - * @created Sep 13, 2010 - * @since 1.0 - */ -public class TwitterOAuth -{ - public static void main(String args[]) - throws Exception - { - if (args.length == 2) - { - final twitter4j.Twitter twitter = new TwitterFactory().getInstance(); - twitter.setOAuthConsumer(args[0], args[1]); - final RequestToken requestToken = twitter.getOAuthRequestToken(); - AccessToken accessToken = null; - final BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); - while (null == accessToken) - { - System.out.println("Open the following URL and grant access to your account:"); - System.out.println(requestToken.getAuthorizationURL()); - System.out.print("Enter the PIN (if available) or just hit enter.[PIN]:"); - final String pin = br.readLine(); - try - { - if (pin.length() > 0) - { - accessToken = twitter.getOAuthAccessToken(requestToken, pin); - } - else - { - accessToken = twitter.getOAuthAccessToken(); - } - - System.out.println( - "Please add the following to the bot's property file:" + "\n\n" + "twitter-consumerKey=" - + args[0] + '\n' + "twitter-consumerSecret=" + args[1] + '\n' + "twitter-token=" - + accessToken.getToken() + '\n' + "twitter-tokenSecret=" + accessToken.getTokenSecret()); - } - catch (TwitterException te) - { - if (401 == te.getStatusCode()) - { - System.out.println("Unable to get the access token."); - } - else - { - te.printStackTrace(); - } - } - } - } - else - { - System.out.println("Usage: " + TwitterOAuth.class.getName() + " <consumerKey> <consumerSecret>"); - } - - System.exit(0); - } -} \ No newline at end of file diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/AddonsTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/AddonsTest.kt new file mode 100644 index 0000000..27163fb --- /dev/null +++ b/src/test/kotlin/net/thauvin/erik/mobibot/AddonsTest.kt @@ -0,0 +1,86 @@ +/* + * AddonsTest.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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 java.util.* +import kotlin.test.Test + +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/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..75a1cf9 --- /dev/null +++ b/src/test/kotlin/net/thauvin/erik/mobibot/DisableOnCi.kt @@ -0,0 +1,44 @@ +/* + * DisableOnCi.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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 DisableOnCi 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..d887b55 --- /dev/null +++ b/src/test/kotlin/net/thauvin/erik/mobibot/DisableOnCiCondition.kt @@ -0,0 +1,51 @@ +/* + * DisableOnCiCondition.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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 new file mode 100644 index 0000000..3a4adb0 --- /dev/null +++ b/src/test/kotlin/net/thauvin/erik/mobibot/ExceptionSanitizer.kt @@ -0,0 +1,60 @@ +/* + * ExceptionSanitizer.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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/src/test/kotlin/net/thauvin/erik/mobibot/FeedReaderTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/FeedReaderTest.kt new file mode 100644 index 0000000..0c3d1c6 --- /dev/null +++ b/src/test/kotlin/net/thauvin/erik/mobibot/FeedReaderTest.kt @@ -0,0 +1,75 @@ +/* + * FeedReaderTest.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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 java.io.IOException +import java.net.MalformedURLException +import java.net.UnknownHostException +import kotlin.test.Test + +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/src/test/kotlin/net/thauvin/erik/mobibot/LocalProperties.kt b/src/test/kotlin/net/thauvin/erik/mobibot/LocalProperties.kt new file mode 100644 index 0000000..646a0ea --- /dev/null +++ b/src/test/kotlin/net/thauvin/erik/mobibot/LocalProperties.kt @@ -0,0 +1,83 @@ +/* + * LocalProperties.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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 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 { + init { + 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/src/test/kotlin/net/thauvin/erik/mobibot/PinboardTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/PinboardTest.kt new file mode 100644 index 0000000..dafb862 --- /dev/null +++ b/src/test/kotlin/net/thauvin/erik/mobibot/PinboardTest.kt @@ -0,0 +1,81 @@ +/* + * PinboardTest.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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 java.net.URL +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +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/src/test/kotlin/net/thauvin/erik/mobibot/UtilsTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/UtilsTest.kt new file mode 100644 index 0000000..bd05f70 --- /dev/null +++ b/src/test/kotlin/net/thauvin/erik/mobibot/UtilsTest.kt @@ -0,0 +1,275 @@ +/* + * UtilsTest.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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.junit.jupiter.api.BeforeEach +import org.pircbotx.Colors +import java.io.File +import java.io.IOException +import java.net.URL +import java.time.LocalDateTime +import java.util.* +import kotlin.test.Test + +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." + + @BeforeEach + 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( + "<a name=\"test & ''\">" + ) + } + + @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/src/test/kotlin/net/thauvin/erik/mobibot/commands/InfoTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/commands/InfoTest.kt new file mode 100644 index 0000000..f332005 --- /dev/null +++ b/src/test/kotlin/net/thauvin/erik/mobibot/commands/InfoTest.kt @@ -0,0 +1,58 @@ +/* + * InfoTest.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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/src/test/kotlin/net/thauvin/erik/mobibot/commands/RecapTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/commands/RecapTest.kt new file mode 100644 index 0000000..ef6f461 --- /dev/null +++ b/src/test/kotlin/net/thauvin/erik/mobibot/commands/RecapTest.kt @@ -0,0 +1,60 @@ +/* + * RecapTest.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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<String>::first) + .matches("[1-2]\\d{3}-[01]\\d-[0-3]\\d [0-2]\\d:[0-6]\\d - sender11: test 11".toRegex()) + prop(MutableList<String>::last) + .matches("[1-2]\\d{3}-[01]\\d-[0-3]\\d [0-2]\\d:[0-6]\\d - sender20: test 20".toRegex()) + } + + Recap.storeRecap("sender", "test action", true) + assertThat(Recap.recaps.last()) + .matches("[1-2]\\d{3}-[01]\\d-[0-3]\\d [0-2]\\d:[0-6]\\d - sender test action".toRegex()) + } +} diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/commands/links/LinksManagerTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/commands/links/LinksManagerTest.kt new file mode 100644 index 0000000..676c5b6 --- /dev/null +++ b/src/test/kotlin/net/thauvin/erik/mobibot/commands/links/LinksManagerTest.kt @@ -0,0 +1,77 @@ +/* + * LinksManagerTest.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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<String>() + + 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/src/test/kotlin/net/thauvin/erik/mobibot/commands/links/ViewTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/commands/links/ViewTest.kt new file mode 100644 index 0000000..abf8224 --- /dev/null +++ b/src/test/kotlin/net/thauvin/erik/mobibot/commands/links/ViewTest.kt @@ -0,0 +1,111 @@ +/* + * ViewTest.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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<Int, String>::first).isEqualTo(0) + prop(Pair<Int, String>::second).isEqualTo("") + } + + assertThat(view.parseArgs("2 foo"), "parseArgs(2, foo)").all { + prop(Pair<Int, String>::first).isEqualTo(1) + prop(Pair<Int, String>::second).isEqualTo("foo") + } + + assertThat(view.parseArgs("3 FOO"), "parseArgs(3, FOO)").all { + prop(Pair<Int, String>::first).isEqualTo(2) + prop(Pair<Int, String>::second).isEqualTo("foo") + } + + assertThat(view.parseArgs(" 4 foo bar "), "parseArgs( 4 foo bar )").all { + prop(Pair<Int, String>::first).isEqualTo(3) + prop(Pair<Int, String>::second).isEqualTo("foo bar") + } + + assertThat(view.parseArgs("foo bar"), "parseArgs(foo bar)").all { + prop(Pair<Int, String>::first).isEqualTo(0) + prop(Pair<Int, String>::second).isEqualTo("foo bar") + } + + assertThat(view.parseArgs("${Int.MAX_VALUE}1"), "parseArgs(overflow)").all { + prop(Pair<Int, String>::first).isEqualTo(0) + prop(Pair<Int, String>::second).isEqualTo("${Int.MAX_VALUE}1") + } + + assertThat(view.parseArgs("1a"), "parseArgs(1a)").all { + prop(Pair<Int, String>::first).isEqualTo(0) + prop(Pair<Int, String>::second).isEqualTo("1a") + } + + assertThat(view.parseArgs("20"), "parseArgs(20)").all { + prop(Pair<Int, String>::first).isEqualTo(0) + prop(Pair<Int, String>::second).isEqualTo("") + } + + assertThat(view.parseArgs(""), "parseArgs()").all { + prop(Pair<Int, String>::first).isEqualTo(LinksManager.entries.links.size - View.MAX_ENTRIES) + prop(Pair<Int, String>::second).isEqualTo("") + } + + LinksManager.entries.links.clear() + + assertThat(view.parseArgs("4"), "parseArgs(4)").all { + prop(Pair<Int, String>::first).isEqualTo(0) + prop(Pair<Int, String>::second).isEqualTo("") + } + } +} 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 new file mode 100644 index 0000000..7b946dc --- /dev/null +++ b/src/test/kotlin/net/thauvin/erik/mobibot/commands/seen/SeenTest.kt @@ -0,0 +1,93 @@ +/* + * SeenTest.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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.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 { + @Test + @Order(1) + fun loadTest() { + seen.clear() + assertThat(seen::seenNicks).isEmpty() + seen.load() + assertThat(seen::seenNicks).key(NICK).isNotNull() + } + + @Test + @Order(2) + 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 + @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 new file mode 100644 index 0000000..443c1f9 --- /dev/null +++ b/src/test/kotlin/net/thauvin/erik/mobibot/commands/tell/TellMessageTest.kt @@ -0,0 +1,69 @@ +/* + * TellMessageTest.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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 java.time.Duration +import java.time.LocalDateTime +import java.time.temporal.Temporal +import kotlin.test.Test + +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/src/test/kotlin/net/thauvin/erik/mobibot/commands/tell/TellMessagesMgrTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/commands/tell/TellMessagesMgrTest.kt new file mode 100644 index 0000000..6d3bb6b --- /dev/null +++ b/src/test/kotlin/net/thauvin/erik/mobibot/commands/tell/TellMessagesMgrTest.kt @@ -0,0 +1,89 @@ +/* + * TellMessagesMgrTest.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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.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 maxDays = 10L + private val testMessages = mutableListOf<TellMessage>().apply { + for (i in 0..5) { + this.add(i, TellMessage("sender$i", "recipient$i", "message $i")) + } + } + + init { + TellManager.save(testFile.toAbsolutePath().toString(), testMessages) + assertThat(testFile.fileSize()).isGreaterThan(0) + } + + @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) + } + } + } + + 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 new file mode 100644 index 0000000..f67a057 --- /dev/null +++ b/src/test/kotlin/net/thauvin/erik/mobibot/entries/EntriesUtilsTest.kt @@ -0,0 +1,91 @@ +/* + * EntriesUtilsTest.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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/src/test/kotlin/net/thauvin/erik/mobibot/entries/EntryLinkTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/entries/EntryLinkTest.kt new file mode 100644 index 0000000..3479108 --- /dev/null +++ b/src/test/kotlin/net/thauvin/erik/mobibot/entries/EntryLinkTest.kt @@ -0,0 +1,126 @@ +/* + * EntryLinkTest.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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 java.security.SecureRandom +import java.util.* +import kotlin.test.Test + +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("<empty>"), "matches(empty)").isFalse() + assertThat(entryLink.matches(null), "matches(null)").isFalse() + } + + + @Test + fun testTags() { + val tags: List<SyndCategory> = 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/src/test/kotlin/net/thauvin/erik/mobibot/entries/FeedMgrTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/entries/FeedMgrTest.kt new file mode 100644 index 0000000..5803092 --- /dev/null +++ b/src/test/kotlin/net/thauvin/erik/mobibot/entries/FeedMgrTest.kt @@ -0,0 +1,113 @@ +/* + * FeedMgrTest.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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 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" + + init { + 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/src/test/kotlin/net/thauvin/erik/mobibot/modules/CalcTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/modules/CalcTest.kt new file mode 100644 index 0000000..140b8a1 --- /dev/null +++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/CalcTest.kt @@ -0,0 +1,50 @@ +/* + * CalcTest.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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 + +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/src/test/kotlin/net/thauvin/erik/mobibot/modules/ChatGpt2Test.kt b/src/test/kotlin/net/thauvin/erik/mobibot/modules/ChatGpt2Test.kt new file mode 100644 index 0000000..ee3e534 --- /dev/null +++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/ChatGpt2Test.kt @@ -0,0 +1,61 @@ +/* + * ChatGpt2Test.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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.DisableOnCi +import net.thauvin.erik.mobibot.LocalProperties +import kotlin.test.Test + +class ChatGpt2Test : LocalProperties() { + @Test + fun testApiKey() { + assertFailure { ChatGpt2.chat("1 gallon to liter", "", 0) } + .isInstanceOf(ModuleException::class.java) + .hasNoCause() + } + + @Test + @DisableOnCi + fun testChat() { + val apiKey = getProperty(ChatGpt2.API_KEY_PROP) + assertThat( + ChatGpt2.chat("how do I make an HTTP request in Javascript?", apiKey, 200) + ).contains("XMLHttpRequest") + + assertFailure { ChatGpt2.chat("1 liter to gallon", apiKey, -1) } + .isInstanceOf(ModuleException::class.java) + } +} diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/modules/CryptoPricesTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/modules/CryptoPricesTest.kt new file mode 100644 index 0000000..94a40d9 --- /dev/null +++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/CryptoPricesTest.kt @@ -0,0 +1,87 @@ +/* + * CryptoPricesTest.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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.junit.jupiter.api.BeforeAll +import java.util.logging.ConsoleHandler +import java.util.logging.Level +import kotlin.test.Test + +class CryptoPricesTest { + init { + 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") + } + + companion object { + @JvmStatic + @BeforeAll + fun beforeAll() { + with(CryptoPrice.logger) { + addHandler(ConsoleHandler().apply { level = Level.FINE }) + level = Level.FINE + useParentHandlers = false + } + } + } +} diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/modules/CurrencyConverterTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/modules/CurrencyConverterTest.kt new file mode 100644 index 0000000..c1c0efc --- /dev/null +++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/CurrencyConverterTest.kt @@ -0,0 +1,77 @@ +/* + * CurrencyConverterTest.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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 kotlin.test.Test + +class CurrencyConverterTest : LocalProperties() { + init { + 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/src/test/kotlin/net/thauvin/erik/mobibot/modules/DiceTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/modules/DiceTest.kt new file mode 100644 index 0000000..e34de7b --- /dev/null +++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/DiceTest.kt @@ -0,0 +1,53 @@ +/* + * DiceTest.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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/src/test/kotlin/net/thauvin/erik/mobibot/modules/Gemini2Test.kt b/src/test/kotlin/net/thauvin/erik/mobibot/modules/Gemini2Test.kt new file mode 100644 index 0000000..269874a --- /dev/null +++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/Gemini2Test.kt @@ -0,0 +1,65 @@ +/* + * Gemini2Test.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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.* +import net.thauvin.erik.mobibot.DisableOnCi +import net.thauvin.erik.mobibot.LocalProperties +import kotlin.test.Test + +class Gemini2Test : LocalProperties() { + @Test + fun testApiKey() { + assertFailure { Gemini2.chat("1 gallon to liter", "", 0) } + .isInstanceOf(ModuleException::class.java) + .hasNoCause() + } + + @Test + @DisableOnCi + fun chatPrompt() { + val apiKey = getProperty(Gemini2.GEMINI_API_KEY) + val maxTokens = getProperty(Gemini2.MAX_TOKENS_PROP).toInt() + + assertThat( + Gemini2.chat("how do I make an HTTP request in Javascript?", apiKey, maxTokens) + ).isNotNull().contains("XMLHttpRequest") + + assertThat( + Gemini2.chat("how do I encode a URL in java?", apiKey, 60) + ).isNotNull().contains("URLEncoder") + + assertFailure { Gemini2.chat("1 liter to gallon", "foo", 40) } + .isInstanceOf(ModuleException::class.java) + } +} diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/modules/GoogleSearchTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/modules/GoogleSearchTest.kt new file mode 100644 index 0000000..f9b0832 --- /dev/null +++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/GoogleSearchTest.kt @@ -0,0 +1,94 @@ +/* + * GoogleSearchTest.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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.DisableOnCi +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 + +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 + @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/src/test/kotlin/net/thauvin/erik/mobibot/modules/JokeTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/modules/JokeTest.kt new file mode 100644 index 0000000..cf6d03c --- /dev/null +++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/JokeTest.kt @@ -0,0 +1,54 @@ +/* + * JokeTest.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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 + +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/src/test/kotlin/net/thauvin/erik/mobibot/modules/LookupTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/modules/LookupTest.kt new file mode 100644 index 0000000..abb9235 --- /dev/null +++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/LookupTest.kt @@ -0,0 +1,57 @@ +/* + * LookupTest.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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 + +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("37.27.52.13") + assertThat(result, "lookup(37.27.52.13)").contains("nix4.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/src/test/kotlin/net/thauvin/erik/mobibot/modules/MastodonTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/modules/MastodonTest.kt new file mode 100644 index 0000000..a9e1d43 --- /dev/null +++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/MastodonTest.kt @@ -0,0 +1,54 @@ +/* + * MastodonTest.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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/src/test/kotlin/net/thauvin/erik/mobibot/modules/ModuleExceptionTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/modules/ModuleExceptionTest.kt new file mode 100644 index 0000000..6c3c54c --- /dev/null +++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/ModuleExceptionTest.kt @@ -0,0 +1,104 @@ +/* + * ModuleExceptionTest.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.MethodSource +import java.io.IOException +import kotlin.test.Test + +class ModuleExceptionTest { + companion object { + const val DEBUG_MESSAGE = "debugMessage" + const val MESSAGE = "message" + + @JvmStatic + fun dataProviders(): List<ModuleException> { + return listOf( + ModuleException(DEBUG_MESSAGE, MESSAGE, IOException("URL http://foobar.com")), + ModuleException(DEBUG_MESSAGE, MESSAGE, IOException("URL http://foobar.com?")), + ModuleException(DEBUG_MESSAGE, MESSAGE) + ) + } + } + + @ParameterizedTest + @MethodSource("dataProviders") + fun testGetDebugMessage(e: ModuleException) { + assertThat(e::debugMessage).isEqualTo(DEBUG_MESSAGE) + } + + @ParameterizedTest + @MethodSource("dataProviders") + 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/src/test/kotlin/net/thauvin/erik/mobibot/modules/PingTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/modules/PingTest.kt new file mode 100644 index 0000000..f51e203 --- /dev/null +++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/PingTest.kt @@ -0,0 +1,51 @@ +/* + * PingTest.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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 + +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/src/test/kotlin/net/thauvin/erik/mobibot/modules/RockPaperScissorsTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/modules/RockPaperScissorsTest.kt new file mode 100644 index 0000000..519037a --- /dev/null +++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/RockPaperScissorsTest.kt @@ -0,0 +1,50 @@ +/* + * RockPaperScissorsTest.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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/src/test/kotlin/net/thauvin/erik/mobibot/modules/StockQuoteTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/modules/StockQuoteTest.kt new file mode 100644 index 0000000..955a267 --- /dev/null +++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/StockQuoteTest.kt @@ -0,0 +1,82 @@ +/* + * StockQuoteTest.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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 + +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/src/test/kotlin/net/thauvin/erik/mobibot/modules/Weather2Test.kt b/src/test/kotlin/net/thauvin/erik/mobibot/modules/Weather2Test.kt new file mode 100644 index 0000000..5d04560 --- /dev/null +++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/Weather2Test.kt @@ -0,0 +1,114 @@ +/* + * Weather2Test.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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 + +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/src/test/kotlin/net/thauvin/erik/mobibot/modules/WolframAlphaTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/modules/WolframAlphaTest.kt new file mode 100644 index 0000000..099f3f9 --- /dev/null +++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/WolframAlphaTest.kt @@ -0,0 +1,79 @@ +/* + * WolframAlphaTest.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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.DisableOnCi +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 + @DisableOnCi + @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/src/test/kotlin/net/thauvin/erik/mobibot/modules/WordTimeTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/modules/WordTimeTest.kt new file mode 100644 index 0000000..396efaf --- /dev/null +++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/WordTimeTest.kt @@ -0,0 +1,67 @@ +/* + * WordTimeTest.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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 java.time.ZoneId +import kotlin.test.Test + +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/src/test/kotlin/net/thauvin/erik/mobibot/msg/MessageTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/msg/MessageTest.kt new file mode 100644 index 0000000..6e85ed1 --- /dev/null +++ b/src/test/kotlin/net/thauvin/erik/mobibot/msg/MessageTest.kt @@ -0,0 +1,109 @@ +/* + * MessageTest.kt + * + * Copyright 2004-2025 Erik C. Thauvin (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/src/test/resources/current.xml b/src/test/resources/current.xml new file mode 100644 index 0000000..8552a9a --- /dev/null +++ b/src/test/resources/current.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<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/website/index.html b/website/index.html new file mode 100644 index 0000000..370c3cf --- /dev/null +++ b/website/index.html @@ -0,0 +1,155 @@ + + + + + mobibot + + + + + + + + +
+

mobibot

+ +

The #mobitopia bot

+ +

The latest version of mobibot is always available via + GitHub. +

+ +

About mobibot

+ +

mobibot is the + #mobitopia IRC channel bot written in Kotlin. +

+ +

mobibot is making extensive use of various open source libraries, including:

+ +

mobibot was written by + Erik C. Thauvin as a replacement for the channel's + original + ChumpBot. +

+ +

Features

+ +

mobibot's main functionality is to + capture URLs posted on the channel. The URLs are automatically gathered into a publishable + RSS feed and saved on Pinboard. +

+ +

Other features include:

+
    +
  • Displaying the latest entries on Mobitopia +
    mobibot: view
    +
  • +
  • Performing calculations +
    mobibot: calc (floor(sqrt(3)) + π) * 3^2
    +
  • +
  • Crypto currencies prices +
    mobibot: cryto btc
    +
    mobibot: cryto eth eur
    +
  • +
  • Converting between currencies +
    mobibot: currency 17.54 USD to EUR
    +
  • +
  • Performing Google searches +
    mobibot: google mobitopia on irc
    +
  • +
  • Getting answers from Wolfram Alpha, ChatGPT and Google Gemini +
    mobibot: wolfram days until christmas
    +
    mobibot: chatgpt explain quantum computing in simple terms
    +
    mobibot: gemini what are all the colors in a rainbow?
    +
  • +
  • Displaying weather information +
    mobibot: weather san francisco
    +
    mobibot: weather 94123
    +
    mobibot: weather tokyo, jp
    +
  • +
  • Performing DNS lookups +
    mobibot: lookup www.apple.com
    +
  • +
  • Retrieving stock quotes from Alpha Avantage +
    mobibot: stock GOOG
    +
    mobibot: stock google
    +
  • +
  • Displaying the time in various time zones +
    mobibot: time UK
    +
    mobibot: time GMT
    +
  • +
  • Sending messages to people on join/activity +
    mobibot: tell nickname Give me a call when you see this.
    +
  • +
  • Recapping public channel messages +
    /msg mobibot recap
    +
  • +
  • Listing the users on the channel +
    /msg mobibot users
    +
  • +
  • Viewing when a nickname was last seen +
    /msg mobibot seen nickname
    +
  • +
  • Random jokes from Sv443's JokeAPI +
    mobibot: joke
    +
  • +
  • Playing dice, war or rock paper scissors +
    mobibot: dice
    +
    mobibot: war
    +
    mobibot: paper
    +
    mobibot: rock
    +
  • +
  • Automatic and manual posting to Mastodon +
    mobibot: toot hello mastodon
    +
  • +
+

Some of the internal features include RSS feed backlogs, rolling logs, debugging toggle and much more.

+ +

If you have any feature suggestions, please post them to the + mobibot wiki. +

+ +

Using mobibot

+ +

To use mobibot, simply join #mobitopia on + irc.libera.chat and type: +

+ +

mobibot: help

+ +

mobibot will reply with a listing of the commands currently supported.

+ +

Licenses

+ +

There are various open source licenses attached to mobibot. Please refer to the + licenses directory + in the source tree for more details. +

+
+ + + diff --git a/website/simple.css b/website/simple.css new file mode 100644 index 0000000..e1204d6 --- /dev/null +++ b/website/simple.css @@ -0,0 +1,52 @@ +body { + background: #ffffff; + font-family: Verdana, Arial, Helvetica, sans-serif; + font-size: 16px; + color: #000000; + margin: 0; +} + +h1 { + color: #333399; +} + +h2 { + border-bottom: 2px solid #9999cc; +} + +h3 { + padding-top: 25px; + border-bottom: 1px solid #9999cc; +} + +code { + font-family: Courier New, Courier, mono, monospace; + color: #000066; + background-color: #ffeedd; + margin-left: 20px +} + +a:hover { + color: #ff0000; + background: #eeeeff; +} + +#content { + float: none; + position: relative; + margin: 0 10px 10px 10px; + padding: 0 10px 10px 10px; +} + +#content p { + padding-left: 10px; +} + +#content p.note { + padding: 5px; + border: 1px solid #9999cc; + border-right: 2px solid #666699; + border-bottom: 2px solid #666699; + background: #eeeeff; + margin: 20px 50px; +} \ No newline at end of file