diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 5d7b6ed..0000000 --- a/.travis.yml +++ /dev/null @@ -1,8 +0,0 @@ -language: java -jdk: - - oraclejdk8 - -addons: - apt: - packages: - - oracle-java8-installer \ No newline at end of file diff --git a/README.md b/README.md index ac78f24..c5a6582 100644 --- a/README.md +++ b/README.md @@ -1,71 +1,22 @@
abilitybots - [![Build Status](https://travis-ci.org/addo37/ExampleBots.svg?branch=master)](https://travis-ci.org/addo37/ExampleBots) - [![Jitpack](https://jitpack.io/v/addo37/ExampleBots.svg)](https://jitpack.io/#addo37/ExampleBots) - [![Telegram](http://trellobot.doomdns.org/telegrambadge.svg)](https://telegram.me/AbilityBots) +[![Jitpack](https://jitpack.io/v/addo37/ExampleBots.svg)](https://jitpack.io/#addo37/ExampleBots) +[![Telegram](https://telegram-badge.vercel.app/api/telegram-badge?channelId=@AbilityBots)](https://telegram.me/AbilityBots)
-Example Bots ------------- +# Example Bots -Your entry point should be the **[ExampleBot](./src/main/java/org/telegram/examplebots/ExampleBot.java)**, it showcases most of the features of the AbilityBot. +Small, up-to-date samples of [telegrambots](https://github.com/rubenlagus/TelegramBots) 9.2 ability bots built with Java 25. -Don't forget that you can do the following with ANY AbilityBot! +## Quick start -* /claim - Claims this bot -* /commands - Reports all user-defined commands (abilities) -* /backup - Returns a backup of the bot database -* /recover - Recovers the database -* /promote @username- Promotes user to bot admin -* /demote @username - Demotes bot admin to user -* /ban @username - Bans the user from accessing your bot commands and features -* /unban @username - Lifts the ban from the user +- Set `BOT_TOKEN` and `BOT_USERNAME` in `src/main/java/org/telegram/examplebots/Application.java`, then choose whether to register `ExampleBot` or the minimal `HelloBot`. +- Run `./gradlew test` to ensure the project compiles and the mocked sender test still passes. +- Launch `Application.main()` from your IDE (or wire the class into your own host) to start long polling your bot. -Sample: +## What's inside -```java -public Ability saysHelloWorld() { - return Ability.builder() - .name("hello") // Name and command (/hello) - .info("Says hello world!") // Necessary if you want it to be reported via /commands - .privacy(PUBLIC) // Choose from Privacy Class (Public, Admin, Creator) - .locality(ALL) // Choose from Locality enum Class (User, Group, PUBLIC) - .input(0) // Arguments required for command (0 for ignore) - .action(ctx -> { - /* - ctx has the following main fields that you can utilize: - - ctx.update() -> the actual Telegram update from the basic API - - ctx.user() -> the user behind the update - - ctx.firstArg()/secondArg()/thirdArg() -> quick accessors for message arguments (if any) - - ctx.arguments() -> all arguments - - ctx.chatId() -> the chat where the update has emerged - - NOTE that chat ID and user are fetched no matter what the update carries. - If the update does not have a message, but it has a callback query, the chatId and user will be fetched from that query. - */ - // Custom sender implementation - sender.send("Hello World!", ctx.chatId()); - }) - .build(); -} -``` - -Testing -------- -Check out the **[ExampleBotTest](./src/test/java/org/telegram/examplebots/ExampleBotTest.java)** on how you can harness the power of mocked senders! - -Sample -```java -@Test -public void canSayHelloWorld() { - Update mockedUpdate = mock(Update.class); - EndUser endUser = EndUser.endUser(USER_ID, "Abbas", "Abou Daya", "addo37"); - MessageContext context = new MessageContext(mockedUpdate, endUser, CHAT_ID); - - bot.saysHelloWorld().consumer().accept(context); - - // We verify that the sender was called only ONCE and sent Hello World to CHAT_ID - Mockito.verify(sender, times(1)).send("Hello World!", CHAT_ID); -} -``` \ No newline at end of file +- `ExampleBot` shows admin/public abilities, forced replies, photo-only handlers, and `/count` storage using the AbilityBot database. Built-in AbilityBot commands such as `/claim`, `/commands`, `/backup`, etc. remain available out of the box. +- `HelloBot` is a bare-bones hello-world ability for stripping the setup down to the essentials. +- `ExampleBotTest` demonstrates how to plug in Mockito and the `SilentSender` to unit test your abilities without calling Telegram. diff --git a/build.gradle b/build.gradle index a54eb10..ffde20e 100644 --- a/build.gradle +++ b/build.gradle @@ -1,9 +1,12 @@ -group 'org.telegram' -version '1.0' - -apply plugin: 'java' +plugins { + id 'java' +} -sourceCompatibility = 1.8 +java { + toolchain { + languageVersion = JavaLanguageVersion.of(25) + } +} repositories { mavenCentral() @@ -11,9 +14,10 @@ repositories { } dependencies { - compile 'org.telegram:telegrambots-abilities:3.4' - compile 'org.telegram:telegrambots:2.4.4.5' + implementation 'org.telegram:telegrambots-abilities:9.2.0' + implementation 'org.telegram:telegrambots-longpolling:9.2.0' + implementation 'org.telegram:telegrambots-client:9.2.0' - testCompile group: 'junit', name: 'junit', version: '4.11' - testCompile 'org.mockito:mockito-all:2.0.2-beta' + testImplementation 'junit:junit:4.13.2' + testImplementation 'org.mockito:mockito-core:5.21.0' } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 6ffa237..f8e1ee3 100755 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 4053666..23449a2 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ -#Sun Feb 19 22:11:08 EET 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-3.1-all.zip diff --git a/gradlew b/gradlew index 9aa616c..adff685 100755 --- a/gradlew +++ b/gradlew @@ -1,78 +1,128 @@ -#!/usr/bin/env bash +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # Attempt to set APP_HOME + # Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum -warn ( ) { +warn () { echo "$*" -} +} >&2 -die ( ) { +die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -81,89 +131,118 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=$((i+1)) + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") -} -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [[ "$(uname)" == "Darwin" ]] && [[ "$HOME" == "$PWD" ]]; then - cd "$(dirname "$0")" +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" fi -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index f955316..e509b2d 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,84 +1,93 @@ -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle index 56ff1a7..7705aee 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1 @@ rootProject.name = 'examplebots' - diff --git a/src/main/java/org/telegram/examplebots/Application.java b/src/main/java/org/telegram/examplebots/Application.java index 3f8e4a9..77c180c 100644 --- a/src/main/java/org/telegram/examplebots/Application.java +++ b/src/main/java/org/telegram/examplebots/Application.java @@ -1,19 +1,36 @@ package org.telegram.examplebots; -import org.telegram.telegrambots.ApiContextInitializer; -import org.telegram.telegrambots.TelegramBotsApi; -import org.telegram.telegrambots.exceptions.TelegramApiRequestException; -import org.telegram.telegrambots.logging.BotLogger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.telegram.telegrambots.longpolling.TelegramBotsLongPollingApplication; +import org.telegram.telegrambots.meta.exceptions.TelegramApiException; + public class Application { - public static void main(String... args) { - ApiContextInitializer.init(); + private static final String BOT_TOKEN = "BOT_TOKEN"; + private static final String BOT_USERNAME = "BOT_USERNAME"; + + private static final Logger log = LoggerFactory.getLogger(Application.class); - TelegramBotsApi api = new TelegramBotsApi(); + static void main() { + // var bot = new HelloBot(BOT_TOKEN, BOT_USERNAME); + var bot = new ExampleBot(BOT_TOKEN, BOT_USERNAME); + bot.onRegister(); + + //noinspection resource + var botsApplication = new TelegramBotsLongPollingApplication(); try { - api.registerBot(new HelloBot()); - } catch (TelegramApiRequestException e) { - BotLogger.error("Oops, something went wrong while registering bot", e); + botsApplication.registerBot(BOT_TOKEN, bot); + } catch (TelegramApiException e) { + log.error("Oops, something went wrong while registering bot", e); } + + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + try { + botsApplication.close(); + } catch (Exception e) { + log.error("Oops, something went wrong while closing application", e); + } + })); } -} \ No newline at end of file +} diff --git a/src/main/java/org/telegram/examplebots/ExampleBot.java b/src/main/java/org/telegram/examplebots/ExampleBot.java index c703c80..96f4007 100644 --- a/src/main/java/org/telegram/examplebots/ExampleBot.java +++ b/src/main/java/org/telegram/examplebots/ExampleBot.java @@ -1,224 +1,227 @@ package org.telegram.examplebots; import com.google.common.annotations.VisibleForTesting; -import org.glassfish.hk2.api.Visibility; -import org.telegram.abilitybots.api.bot.AbilityBot; -import org.telegram.abilitybots.api.objects.Ability; -import org.telegram.abilitybots.api.sender.MessageSender; -import org.telegram.telegrambots.api.objects.Message; -import org.telegram.telegrambots.api.objects.Update; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.telegram.telegrambots.abilitybots.api.bot.AbilityBot; +import org.telegram.telegrambots.abilitybots.api.objects.Ability; +import org.telegram.telegrambots.abilitybots.api.sender.SilentSender; +import org.telegram.telegrambots.client.okhttp.OkHttpTelegramClient; +import org.telegram.telegrambots.meta.api.objects.Update; +import org.telegram.telegrambots.meta.api.objects.message.Message; import java.util.Map; import java.util.function.Predicate; -import static org.telegram.abilitybots.api.objects.Flag.*; -import static org.telegram.abilitybots.api.objects.Locality.ALL; -import static org.telegram.abilitybots.api.objects.Locality.USER; -import static org.telegram.abilitybots.api.objects.Privacy.ADMIN; -import static org.telegram.abilitybots.api.objects.Privacy.PUBLIC; +import static org.telegram.telegrambots.abilitybots.api.objects.Flag.MESSAGE; +import static org.telegram.telegrambots.abilitybots.api.objects.Flag.PHOTO; +import static org.telegram.telegrambots.abilitybots.api.objects.Flag.REPLY; +import static org.telegram.telegrambots.abilitybots.api.objects.Locality.ALL; +import static org.telegram.telegrambots.abilitybots.api.objects.Locality.USER; +import static org.telegram.telegrambots.abilitybots.api.objects.Privacy.ADMIN; +import static org.telegram.telegrambots.abilitybots.api.objects.Privacy.PUBLIC; public class ExampleBot extends AbilityBot { - public static final String BOT_TOKEN = "BOT_TOKEN"; - public static final String BOT_USERNAME = "BOT_USERNAME"; - - public ExampleBot() { - super(BOT_TOKEN, BOT_USERNAME); - } - - @Override - public int creatorId() { - return 1337; // Your ID here - } - - public Ability saysHelloWorld() { - return Ability.builder() - .name("hello") // Name and command (/hello) - .info("Says hello world!") // Necessary if you want it to be reported via /commands - .privacy(PUBLIC) // Choose from Privacy Class (Public, Admin, Creator) - .locality(ALL) // Choose from Locality enum Class (User, Group, PUBLIC) - .input(0) // Arguments required for command (0 for ignore) - .action(ctx -> { - /* - ctx has the following main fields that you can utilize: - - ctx.update() -> the actual Telegram update from the basic API - - ctx.user() -> the user behind the update - - ctx.firstArg()/secondArg()/thirdArg() -> quick accessors for message arguments (if any) - - ctx.arguments() -> all arguments - - ctx.chatId() -> the chat where the update has emerged - - NOTE that chat ID and user are fetched no matter what the update carries. - If the update does not have a message, but it has a callback query, the chatId and user will be fetched from that query. - */ - // Custom sender implementation - sender.send("Hello World!", ctx.chatId()); - }) - .build(); - } - - /** - * Says hi with the specified argument. - *

- * You can experiment by using /sayhi developer. You can also try not supplying it an argument. :) - *

- * Note that this ability only works in USER locality mode. So, it won't work in groups! - */ - public Ability saysHelloWorldToFriend() { - return Ability.builder() - .name("sayhi") - .info("Says hi") - .privacy(PUBLIC) - .locality(USER) - .input(1) - .action(ctx -> sender.send("Hi " + ctx.firstArg(), ctx.chatId())) - .build(); - } - - - /** - * Admin-only ability. Use /scared to invoke in any chat. - *

- * To use this ability, do /claim then /scared! /claim will make you the creator of the bot and automatically an admin. - */ - public Ability scaredOfAdmins() { - return Ability.builder() - .name("scared") - .info("Makes me scared of admins!") - .privacy(ADMIN) - .locality(ALL) - .input(0) - .action(ctx -> { - // Custom sender implementation - sender.send("Eeeeek, I'm so sorry your highness!", ctx.chatId()); - }) - .post(ctx -> { - // This "post" action is executed after the main action is done - sender.send("I will never do it again!", ctx.chatId()); - }) - .build(); - } - - /** - * This is very important to override for {@link ExampleBot#sayNiceToPhoto()}. By default, any update that does not have a message will not pass through abilities. - * To customize that, you can just override this global flag and make it return true at every update. - * This way, the ability flags will be the only ones responsible for checking the update's validity. - */ - @Override - public boolean checkGlobalFlags(Update update) { - return true; - } - - /** - * This ability has an extra "flag". It needs a photo to activate! This feature is activated by default if there is no /command given. - */ - public Ability sayNiceToPhoto() { - return Ability.builder() - .name(DEFAULT) // DEFAULT ability is executed if user did not specify a command -> Bot needs to have access to messages (check FatherBot) - .flag(PHOTO) - .privacy(PUBLIC) - .locality(ALL) - .input(0) - .action(ctx -> sender.send("Daaaaang, what a nice photo!", ctx.chatId())) - .build(); - } - - /** - * Use the database to fetch a count per user and increments. - *

- * Use /count to experiment with this ability. - */ - public Ability useDatabaseToCountPerUser() { - return Ability.builder() - .name("count") - .info("Increments a counter per user") - .privacy(PUBLIC) - .locality(ALL) - .input(0) - .action(ctx -> { - // db.getMap takes in a string, this must be unique and the same everytime you want to call the exact same map - // TODO: Using integer as a key in this db map is not recommended, it won't be serialized/deserialized properly if you ever decide to recover/backup db - Map countMap = db.getMap("COUNTERS"); - int userId = ctx.user().id(); - - // Get and increment counter, put it back in the map - Integer counter = countMap.compute(String.valueOf(userId), (id, count) -> count == null ? 1 : ++count); - - /* - - Without lambdas implementation of incrementing - - int counter; - if (countMap.containsKey(userId)) - counter = countMap.get(userId) + 1; - else - counter = 1; - countMap.put(userId, counter); - - */ - - // Send formatted will enable markdown - String message = String.format("%s, your count is now *%d*!", ctx.user().shortName(), counter); - sender.sendMd(message, ctx.chatId()); - }) - .build(); - - // In this ability, you can also experiment with /backup and /recover of the AbilityBot! - // Take a backup of when the counter is at 1, do /count multiple times and attempt to recover - // The counter will be reset since the db will recover to the backup that you specify - } - - public Ability playWithMe() { - String playMessage = "Play with me!"; - - return Ability.builder() - .name("play") - .info("Do you want to play with me?") - .privacy(PUBLIC) - .locality(ALL) - .input(0) - .action(ctx -> sender.forceReply(playMessage, ctx.chatId())) - // The signature of a reply is -> (Consumer action, Predicate... conditions) - // So, we first declare the action that takes an update (NOT A MESSAGECONTEXT) like the action above - // The reason of that is that a reply can be so versatile depending on the message, context becomes an inefficient wrapping - .reply(upd -> { - // Prints to console - System.out.println("I'm in a reply!"); - // Sends message - sender.send("It's been nice playing with you!", upd.getMessage().getChatId()); - }, - // Now we start declaring conditions, MESSAGE is a member of the enum Flag class - // That class contains out-of-the-box predicates for your replies! - // MESSAGE means that execute the reply if it has a message - MESSAGE, - // REPLY means that the update must be a reply - REPLY, - // The reply must be to the bot - isReplyToBot(), - // The reply is to the playMessage - isReplyToMessage(playMessage) - ) - // You can add more replies by calling .reply(...) - .build(); + private static final Logger log = LoggerFactory.getLogger(ExampleBot.class); + + public ExampleBot(String botToken, String botUsername) { + super(new OkHttpTelegramClient(botToken), botUsername); + } + + @Override + public long creatorId() { + return 330178816; // Your ID here + } + + public Ability saysHelloWorld() { + return Ability.builder() + .name("hello") // Name and command (/hello) + .info("Says hello world!") // Necessary if you want it to be reported via /commands + .privacy(PUBLIC) // Choose from Privacy Class (PUBLIC, GROUP_ADMIN, ADMIN, CREATOR) + .locality(ALL) // Choose from Locality enum Class (USER, GROUP, ALL) + .input(0) // Arguments required for command (0 for ignore) + .action(ctx -> { + /* + ctx has the following main fields that you can utilize: + - ctx.update() -> the actual Telegram update from the basic API + - ctx.user() -> the user behind the update + - ctx.firstArg()/secondArg()/thirdArg() -> quick accessors for message arguments (if any) + - ctx.arguments() -> all arguments + - ctx.chatId() -> the chat where the update has emerged + + NOTE that chat ID and user are fetched no matter what the update carries. + If the update does not have a message, but it has a callback query, the chatId and user will be fetched from that query. + */ + // Custom sender implementation + silent.send("Hello World!", ctx.chatId()); + }) + .build(); + } + + /** + * Says hi with the specified argument. + *

+ * You can experiment by using /sayhi developer. You can also try not supplying it an argument. :) + *

+ * Note that this ability only works in USER locality mode. So, it won't work in groups! + */ + public Ability saysHelloWorldToFriend() { + return Ability.builder() + .name("sayhi") + .info("Says hi") + .privacy(PUBLIC) + .locality(USER) + .input(1) + .action(ctx -> silent.send("Hi " + ctx.firstArg(), ctx.chatId())) + .build(); + } + + + /** + * Admin-only ability. Use /scared to invoke in any chat. + *

+ * To use this ability, do /claim then /scared! /claim will make you the creator of the bot and automatically an admin. + */ + public Ability scaredOfAdmins() { + return Ability.builder() + .name("scared") + .info("Makes me scared of admins!") + .privacy(ADMIN) + .locality(ALL) + .input(0) + .action(ctx -> { + // Custom sender implementation + silent.send("Eeeeek, I'm so sorry your highness!", ctx.chatId()); + }) + .post(ctx -> { + // This "post" action is executed after the main action is done + silent.send("I will never do it again!", ctx.chatId()); + }) + .build(); + } + + /** + * This is very important to override for {@link ExampleBot#sayNiceToPhoto()}. By default, any update that does not have a message will not pass through abilities. + * To customize that, you can just override this global flag and make it return true at every update. + * This way, the ability flags will be the only ones responsible for checking the update's validity. + */ + @Override + public boolean checkGlobalFlags(Update update) { + return true; + } + + /** + * This ability has an extra "flag". It needs a photo to activate! This feature is activated by default if there is no /command given. + */ + public Ability sayNiceToPhoto() { + return Ability.builder() + .name(DEFAULT) // DEFAULT ability is executed if user did not specify a command -> Bot needs to have access to messages (check FatherBot) + .flag(PHOTO) + .privacy(PUBLIC) + .locality(ALL) + .input(0) + .action(ctx -> silent.send("Daaaaang, what a nice photo!", ctx.chatId())) + .build(); + } + + /** + * Use the database to fetch a count per user and increments. + *

+ * Use /count to experiment with this ability. + */ + public Ability useDatabaseToCountPerUser() { + return Ability.builder() + .name("count") + .info("Increments a counter per user") + .privacy(PUBLIC) + .locality(ALL) + .input(0) + .action(ctx -> { + // db.getMap takes in a string, this must be unique and the same everytime you want to call the exact same map + // TODO: Using integer as a key in this db map is not recommended, it won't be serialized/deserialized properly if you ever decide to recover/backup db + Map countMap = db.getMap("COUNTERS"); + long userId = ctx.user().getId(); + + // Get and increment counter, put it back in the map + Integer counter = countMap.compute(String.valueOf(userId), (id, count) -> count == null ? 1 : ++count); + + /* + + Without lambdas implementation of incrementing + + int counter; + if (countMap.containsKey(userId)) + counter = countMap.get(userId) + 1; + else + counter = 1; + countMap.put(userId, counter); + + */ + + // Send formatted will enable markdown + String message = String.format("%s, your count is now *%d*!", ctx.user().getUserName(), counter); + silent.sendMd(message, ctx.chatId()); + }) + .build(); + + // In this ability, you can also experiment with /backup and /recover of the AbilityBot! + // Take a backup of when the counter is at 1, do /count multiple times and attempt to recover + // The counter will be reset since the db will recover to the backup that you specify + } + + public Ability playWithMe() { + String playMessage = "Play with me!"; + + return Ability.builder() + .name("play") + .info("Do you want to play with me?") + .privacy(PUBLIC) + .locality(ALL) + .input(0) + .action(ctx -> silent.forceReply(playMessage, ctx.chatId())) + // The signature of a reply is -> (Consumer action, Predicate... conditions) + // So, we first declare the action that takes an update (NOT A MESSAGECONTEXT) like the action above + // The reason of that is that a reply can be so versatile depending on the message, context becomes an inefficient wrapping + .reply((_, upd) -> { + // Prints to console + log.info("I'm in a reply!"); + // Sends message + silent.send("It's been nice playing with you!", upd.getMessage().getChatId()); + }, + // Now we start declaring conditions, MESSAGE is a member of the enum Flag class + // That class contains out-of-the-box predicates for your replies! + // MESSAGE means that execute the reply if it has a message + MESSAGE, + // REPLY means that the update must be a reply + REPLY, + // The reply must be to the bot + isReplyToBot(), + // The reply is to the playMessage + isReplyToMessage(playMessage) + ) + // You can add more replies by calling .reply(...) + .build(); /* The checks are made so that, once you execute your logic there is not need to check for the validity of the reply. They were all made once the action logic is being executed. */ - } - - private Predicate isReplyToMessage(String message) { - return upd -> { - Message reply = upd.getMessage().getReplyToMessage(); - return reply.hasText() && reply.getText().equalsIgnoreCase(message); - }; - } - - private Predicate isReplyToBot() { - return upd -> upd.getMessage().getReplyToMessage().getFrom().getUserName().equalsIgnoreCase(getBotUsername()); - } - - @VisibleForTesting - void setSender(MessageSender sender) { - this.sender = sender; - } + } + + private Predicate isReplyToMessage(String message) { + return upd -> { + Message reply = upd.getMessage().getReplyToMessage(); + return reply.hasText() && reply.getText().equalsIgnoreCase(message); + }; + } + + private Predicate isReplyToBot() { + return upd -> upd.getMessage().getReplyToMessage().getFrom().getUserName().equalsIgnoreCase(getBotUsername()); + } + + @VisibleForTesting + void setSender(SilentSender silent) { + this.silent = silent; + } } diff --git a/src/main/java/org/telegram/examplebots/HelloBot.java b/src/main/java/org/telegram/examplebots/HelloBot.java index 1bbd914..bc8a00b 100644 --- a/src/main/java/org/telegram/examplebots/HelloBot.java +++ b/src/main/java/org/telegram/examplebots/HelloBot.java @@ -1,32 +1,31 @@ package org.telegram.examplebots; -import org.telegram.abilitybots.api.bot.AbilityBot; -import org.telegram.abilitybots.api.objects.Ability; +import org.telegram.telegrambots.abilitybots.api.bot.AbilityBot; +import org.telegram.telegrambots.abilitybots.api.objects.Ability; +import org.telegram.telegrambots.client.okhttp.OkHttpTelegramClient; -import static org.telegram.abilitybots.api.objects.Locality.ALL; -import static org.telegram.abilitybots.api.objects.Privacy.PUBLIC; +import static org.telegram.telegrambots.abilitybots.api.objects.Locality.ALL; +import static org.telegram.telegrambots.abilitybots.api.objects.Privacy.PUBLIC; public class HelloBot extends AbilityBot { - public static final String BOT_TOKEN = "HELLOBOT_TOKEN"; - public static final String BOT_USERNAME = "HELLOBOT_USERNAME"; - public HelloBot() { - super(BOT_TOKEN, BOT_USERNAME); - } + public HelloBot(String botToken, String botUsername) { + super(new OkHttpTelegramClient(botToken), botUsername); + } - @Override - public int creatorId() { - return 123456789; - } + @Override + public long creatorId() { + return 123456789; + } - public Ability sayHelloWorld() { - return Ability - .builder() - .name("hello") - .info("says hello world!") - .locality(ALL) - .privacy(PUBLIC) - .action(ctx -> silent.send("Hello world!", ctx.chatId())) - .build(); - } + public Ability sayHelloWorld() { + return Ability + .builder() + .name("hello") + .info("says hello world!") + .locality(ALL) + .privacy(PUBLIC) + .action(ctx -> silent.send("Hello world!", ctx.chatId())) + .build(); + } } diff --git a/src/test/java/org/telegram/examplebots/ExampleBotTest.java b/src/test/java/org/telegram/examplebots/ExampleBotTest.java index b4526b1..763186e 100644 --- a/src/test/java/org/telegram/examplebots/ExampleBotTest.java +++ b/src/test/java/org/telegram/examplebots/ExampleBotTest.java @@ -1,51 +1,43 @@ package org.telegram.examplebots; -import org.junit.After; import org.junit.Before; import org.junit.Test; import org.mockito.Mockito; -import org.telegram.abilitybots.api.db.DBContext; -import org.telegram.abilitybots.api.db.MapDBContext; -import org.telegram.abilitybots.api.objects.EndUser; -import org.telegram.abilitybots.api.objects.MessageContext; -import org.telegram.abilitybots.api.sender.MessageSender; -import org.telegram.telegrambots.api.objects.Update; +import org.telegram.telegrambots.abilitybots.api.objects.MessageContext; +import org.telegram.telegrambots.abilitybots.api.sender.SilentSender; +import org.telegram.telegrambots.meta.api.objects.Update; +import org.telegram.telegrambots.meta.api.objects.User; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; public class ExampleBotTest { - public static final int USER_ID = 1337; - public static final long CHAT_ID = 1337L; + public static final int USER_ID = 1337; + public static final long CHAT_ID = 1337L; - private ExampleBot bot; - private DBContext db; - private MessageSender sender; + private ExampleBot bot; + private SilentSender sender; - @Before - public void setUp() { - // Offline instance will get deleted at JVM shutdown - db = MapDBContext.offlineInstance("test"); - bot = new ExampleBot(); - sender = mock(MessageSender.class); + @Before + public void setUp() { + bot = new ExampleBot("TEST_TOKEN", "TEST_USERNAME"); + sender = mock(SilentSender.class); - bot.setSender(sender); - } + bot.setSender(sender); + } - @Test - public void canSayHelloWorld() { - Update mockedUpdate = mock(Update.class); - EndUser endUser = EndUser.endUser(USER_ID, "Abbas", "Abou Daya", "addo37"); - MessageContext context = MessageContext.newContext(mockedUpdate, endUser, CHAT_ID); + @Test + public void canSayHelloWorld() { + Update mockedUpdate = mock(Update.class); + User user = new User((long) USER_ID, "Abbas", false); + user.setLastName("Abou Daya"); + user.setUserName("addo37"); + MessageContext context = MessageContext.newContext(mockedUpdate, user, CHAT_ID, bot); - bot.saysHelloWorld().action().accept(context); + bot.saysHelloWorld().action().accept(context); - // We verify that the sender was called only ONCE and sent Hello World to CHAT_ID - Mockito.verify(sender, times(1)).send("Hello World!", CHAT_ID); - } - - @After - public void tearDown() { - db.clear(); - } -} \ No newline at end of file + // We verify that the sender was called only ONCE and sent Hello World to CHAT_ID + Mockito.verify(sender, times(1)).send("Hello World!", CHAT_ID); + } +}