├── .gitignore ├── .travis.yml ├── README.md ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── library ├── .gitignore ├── build.gradle └── src │ ├── main │ └── java │ │ └── ru │ │ └── mail │ │ └── im │ │ └── botapi │ │ ├── BotApiClient.java │ │ ├── BotApiClientController.java │ │ ├── BotLogger.java │ │ ├── api │ │ ├── Api.java │ │ ├── ApiImplementationFactory.java │ │ ├── BotApi.java │ │ ├── Chats.java │ │ ├── Events.java │ │ ├── GetRequest.java │ │ ├── Messages.java │ │ ├── OkHttpRequestExecutor.java │ │ ├── PostRequest.java │ │ ├── QueryBuilder.java │ │ ├── QueryParameter.java │ │ ├── RequestExecutor.java │ │ ├── RequestParam.java │ │ ├── Self.java │ │ └── entity │ │ │ ├── AnswerCallbackQueryRequest.java │ │ │ ├── DeleteMessagesRequest.java │ │ │ ├── EditTextRequest.java │ │ │ ├── InlineKeyboardButton.java │ │ │ ├── SendFileRequest.java │ │ │ └── SendTextRequest.java │ │ ├── entity │ │ ├── Admin.java │ │ ├── Buddy.java │ │ ├── ChatAction.java │ │ ├── ChatType.java │ │ └── Photo.java │ │ ├── fetcher │ │ ├── BackoffFetcher.java │ │ ├── Chat.java │ │ ├── EventDeserializer.java │ │ ├── FetchResponse.java │ │ ├── Fetcher.java │ │ ├── Message.java │ │ ├── OnEventFetchListener.java │ │ ├── PartDeserializer.java │ │ ├── User.java │ │ └── event │ │ │ ├── CallbackQueryEvent.java │ │ │ ├── DeletedMessageEvent.java │ │ │ ├── EditedMessageEvent.java │ │ │ ├── Event.java │ │ │ ├── EventVisitor.java │ │ │ ├── LeftChatMembersEvent.java │ │ │ ├── NewChatMembersEvent.java │ │ │ ├── NewMessageEvent.java │ │ │ ├── PinnedMessageEvent.java │ │ │ ├── UnknownEvent.java │ │ │ ├── UnpinnedMessageEvent.java │ │ │ └── parts │ │ │ ├── File.java │ │ │ ├── Forward.java │ │ │ ├── Mention.java │ │ │ ├── Part.java │ │ │ ├── Reply.java │ │ │ ├── Sticker.java │ │ │ └── Voice.java │ │ ├── json │ │ └── ChatTypeGsonDeserializer.java │ │ ├── response │ │ ├── ApiResponse.java │ │ ├── ChatsGetAdminsResponse.java │ │ ├── ChatsGetInfoResponse.java │ │ ├── MessageResponse.java │ │ └── SelfGetResponse.java │ │ └── util │ │ ├── IOBackoff.java │ │ ├── ListenerDescriptor.java │ │ └── ListenerList.java │ └── test │ └── java │ └── ru │ └── mail │ └── im │ └── botapi │ ├── api │ ├── ApiImplementationFactoryTest.java │ ├── QueryBuilderTest.java │ └── QueryParameterTest.java │ └── util │ ├── IOBackoffTest.java │ └── ListenerListTest.java ├── sample ├── .gitignore ├── build.gradle └── src │ └── main │ └── java │ └── ru │ └── mail │ └── im │ └── botapi │ └── sample │ ├── AppCommandHandler.java │ ├── FileFetchWriter.java │ ├── Main.java │ ├── OnRequestExecuteListener.java │ ├── PlaceholderValueProviderImpl.java │ ├── ResponsePrinter.java │ ├── SimpleStart.java │ └── command │ ├── CommandHandler.java │ ├── CommandProcessor.java │ └── PlaceholderValueProvider.java └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .gradle 3 | *.iml 4 | local.properties -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: 3 | - openjdk8 4 | 5 | script: 6 | - ./gradlew check 7 | 8 | after_success: 9 | - bash <(curl -s https://codecov.io/bash) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Java interface for bot API 4 | 5 | [![Build Status](https://travis-ci.org/mail-ru-im/bot-java.svg?branch=master)](https://travis-ci.org/mail-ru-im/bot-java) 6 | [![codecov](https://codecov.io/gh/mail-ru-im/bot-java/branch/master/graph/badge.svg)](https://codecov.io/gh/mail-ru-im/bot-java) 7 | 8 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/ru.mail.im/bot-api/badge.svg)](https://maven-badges.herokuapp.com/maven-central/ru.mail.im/bot-api) 9 | 10 | ## Install 11 | 12 | Group ids `io.github.mail-ru-im` and `ru.mail` will no longer being maintained. We moved this artifact to the `ru.mail.im` group id. 13 | 14 | ### Maven 15 | ```xml 16 | 17 | ... 18 | 19 | mavencentral 20 | Maven Central Repository 21 | http://repo1.maven.org/maven2 22 | 23 | ... 24 | 25 | 26 | 27 | ... 28 | 29 | ru.mail.im 30 | bot-api 31 | 1.2.3 32 | 33 | ... 34 | 35 | 36 | ``` 37 | ### Gradle 38 | ```groovy 39 | repositories { 40 | mavenCentral() 41 | } 42 | 43 | dependencies { 44 | implementation 'ru.mail.im:bot-api:1.2.3' 45 | } 46 | ``` 47 | 48 | ## Usage 49 | 50 | Create your own bot by sending the /newbot command to Metabot and follow the instructions. 51 | 52 | Note a bot can only reply after the user has added it to his contacts list, or if the user was the first to start a dialogue. 53 | 54 | ### Create your bot 55 | 56 | ```java 57 | 58 | // For ICQ New/Agent: create bot with token received from Metabot 59 | BotApiClient client = new BotApiClient(token); 60 | // For Myteam: create bot with token from Metabot and host url (for example `https://myteam.mail.ru/`) 61 | BotApiClient client = new BotApiClient(apiBaseUrl, token); 62 | 63 | BotApiClientController controller = BotApiClientController.startBot(client); 64 | client.addOnEventFetchListener(events -> { 65 | //subscribe to new events 66 | }); 67 | 68 | // send actions 69 | controller.sendActions(chatId, ChatAction.TYPING); 70 | 71 | // send message 72 | long messageId = controller.sendTextMessage( 73 | new SendTextRequest() 74 | .setChatId(chatId) 75 | .setText("Bot message") 76 | ).getMsgId(); 77 | 78 | // edit message 79 | controller.editText( 80 | new EditTextRequest() 81 | .setChatId(chatId) 82 | .setMsgId(messageId) 83 | .setNewText("Edited bot message") 84 | ); 85 | 86 | // reply message 87 | controller.sendTextMessage( 88 | new SendTextRequest() 89 | .setChatId(chatId) 90 | .setText("Reply msg") 91 | .setReplyMsgId(Collections.singletonList(messageId)) 92 | ); 93 | 94 | File file = new File("myfile.txt"); 95 | // send file 96 | controller.sendFile( 97 | new SendFileRequest() 98 | .setChatId(chatId) 99 | .setFile(file) 100 | ); 101 | 102 | // reply file 103 | MessageResponse sendFileResponse = controller.sendFile( 104 | new SendFileRequest() 105 | .setChatId(chatId) 106 | .setFile(file) 107 | .setCaption("Awesome file") 108 | .setReplyMsgId(Collections.singletonList(messageId)) 109 | ); 110 | if (!sendFileResponse.isOk()) { // sent successfully or not 111 | // and get the error description if sending failed 112 | String errorDescription = sendFileResponse.getDescription(); 113 | } 114 | 115 | client.stop(); // stop when work done 116 | ``` 117 | 118 | ## Changelog 119 | 120 | `1.2.3` 121 | - Catch all exceptions in IOBackoff.execute 122 | 123 | `1.2.2` 124 | - Support text formats: MarkdownV2, HTML 125 | - Support button's style for inline keyboard 126 | - Moved from JCenter with `ru.mail` group id to Maven Central with `ru.mail.im` group id 127 | 128 | `1.2.1` 129 | - Api response status check possibility 130 | 131 | `1.2.0` 132 | - Support inline keyboards 133 | - Moved from `io.github.mail-ru-im` to `ru.mail` group id 134 | 135 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.versions = [ 3 | 'junit' : '4.12', 4 | 'jsr305' : '3.0.2', 5 | 'okhttp' : '3.14.1', 6 | 'gson' : '2.8.5', 7 | 'slf4j' : '1.7.28', 8 | 'javaVersion': JavaVersion.VERSION_1_8 9 | ] 10 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mail-ru-im/bot-java/915d8d0579c8cbedc73061f72be542266b6e83f0/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Apr 16 14:59:49 MSK 2019 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.0-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /library/.gitignore: -------------------------------------------------------------------------------- 1 | /out 2 | /build -------------------------------------------------------------------------------- /library/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "maven" 3 | id "maven-publish" 4 | id "java" 5 | id "jacoco" 6 | id "signing" 7 | } 8 | 9 | targetCompatibility = versions.javaVersion 10 | sourceCompatibility = versions.javaVersion 11 | 12 | group = "ru.mail.im" 13 | archivesBaseName = "bot-api" 14 | version = "1.2.3" 15 | 16 | repositories { 17 | mavenCentral() 18 | } 19 | 20 | jacocoTestReport { 21 | reports { 22 | xml.enabled = true 23 | html.enabled = false 24 | } 25 | } 26 | 27 | check.dependsOn jacocoTestReport 28 | 29 | dependencies { 30 | compile "com.google.code.findbugs:jsr305:${versions.jsr305}" 31 | compile "com.squareup.okhttp3:okhttp:${versions.okhttp}" 32 | compile "com.google.code.gson:gson:${versions.gson}" 33 | compile "org.slf4j:slf4j-api:${versions.slf4j}" 34 | 35 | testCompile "junit:junit:${versions.junit}" 36 | } 37 | 38 | task sourcesJar(type: Jar, dependsOn: classes) { 39 | classifier = 'sources' 40 | from sourceSets.main.allSource 41 | } 42 | 43 | javadoc.failOnError = false 44 | task javadocJar(type: Jar, dependsOn: javadoc) { 45 | classifier = 'javadoc' 46 | from javadoc.destinationDir 47 | } 48 | 49 | artifacts { 50 | archives sourcesJar 51 | archives javadocJar 52 | } 53 | 54 | signing { 55 | useGpgCmd() 56 | sign configurations.archives 57 | } 58 | 59 | File propsFile = project.rootProject.file('local.properties') 60 | if (propsFile.exists()) { 61 | Properties properties = new Properties() 62 | properties.load(propsFile.newDataInputStream()) 63 | uploadArchives { 64 | repositories { 65 | mavenDeployer { 66 | beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } 67 | 68 | repository(url: "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/") { 69 | authentication( 70 | userName: properties['sonatype.ossrhUsername'], 71 | password: properties['sonatype.ossrhPassword'] 72 | ) 73 | } 74 | 75 | snapshotRepository(url: "https://s01.oss.sonatype.org/content/repositories/snapshots/") { 76 | authentication( 77 | userName: properties['sonatype.ossrhUsername'], 78 | password: properties['sonatype.ossrhPassword'] 79 | ) 80 | } 81 | 82 | pom.project { 83 | name 'bot-api' 84 | packaging 'jar' 85 | // optionally artifactId can be defined here 86 | description 'Java interface for bot API' 87 | url 'https://github.com/mail-ru-im/bot-java' 88 | 89 | scm { 90 | connection 'scm:git:https://github.com/mail-ru-im/bot-java' 91 | developerConnection 'scm:git:https://github.com/mail-ru-im/bot-java' 92 | url 'https://github.com/mail-ru-im/bot-java' 93 | } 94 | 95 | licenses { 96 | license { 97 | name "MIT License" 98 | url "http://www.opensource.org/licenses/mit-license.php" 99 | distribution "repo" 100 | } 101 | } 102 | 103 | developers { 104 | developer { 105 | id properties['developer.id'] 106 | name properties['developer.name'] 107 | email properties['developer.email'] 108 | } 109 | } 110 | } 111 | } 112 | } 113 | } 114 | } else { 115 | println("There is no local.properties file included") 116 | } 117 | -------------------------------------------------------------------------------- /library/src/main/java/ru/mail/im/botapi/BotApiClient.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi; 2 | 3 | import com.google.gson.Gson; 4 | import okhttp3.OkHttpClient; 5 | import ru.mail.im.botapi.api.Api; 6 | import ru.mail.im.botapi.api.BotApi; 7 | import ru.mail.im.botapi.api.Chats; 8 | import ru.mail.im.botapi.api.Messages; 9 | import ru.mail.im.botapi.api.Self; 10 | import ru.mail.im.botapi.fetcher.BackoffFetcher; 11 | import ru.mail.im.botapi.fetcher.Fetcher; 12 | import ru.mail.im.botapi.fetcher.OnEventFetchListener; 13 | import ru.mail.im.botapi.util.ListenerDescriptor; 14 | import ru.mail.im.botapi.util.ListenerList; 15 | 16 | import javax.annotation.Nonnull; 17 | import java.util.concurrent.TimeUnit; 18 | import java.util.concurrent.atomic.AtomicBoolean; 19 | 20 | public class BotApiClient { 21 | 22 | private static final String ICQ_BOT_API_URL = "https://api.icq.net"; 23 | 24 | private final long lastEventId; 25 | private final long pollTime; 26 | 27 | private final AtomicBoolean started = new AtomicBoolean(); 28 | private final ListenerList fetchListenerList; 29 | 30 | private final Api api; 31 | private Fetcher fetcher; 32 | 33 | public BotApiClient(@Nonnull final String token) { 34 | this(ICQ_BOT_API_URL, token); 35 | } 36 | 37 | public BotApiClient( 38 | final String apiUrl, 39 | final String token 40 | ) { 41 | this(apiUrl, token, 0, 60); 42 | } 43 | 44 | public BotApiClient( 45 | final String token, 46 | final long lastEventId, 47 | final long pollTime 48 | ) { 49 | this(ICQ_BOT_API_URL, token, lastEventId, pollTime); 50 | } 51 | 52 | public BotApiClient( 53 | final String apiBaseUrl, 54 | final String token, 55 | final long lastEventId, 56 | final long pollTime 57 | ) { 58 | this(new Gson(), apiBaseUrl, token, lastEventId, pollTime); 59 | } 60 | 61 | public BotApiClient( 62 | final Gson gson, 63 | final String apiBaseUrl, 64 | final String token, 65 | final long lastEventId, 66 | final long pollTime 67 | ) { 68 | this( 69 | gson, 70 | new OkHttpClient.Builder().readTimeout(pollTime, TimeUnit.SECONDS).build(), 71 | new ListenerList<>(OnEventFetchListener.class), 72 | apiBaseUrl, 73 | token, 74 | lastEventId, 75 | pollTime 76 | ); 77 | } 78 | 79 | public BotApiClient( 80 | final Gson gson, 81 | final OkHttpClient httpClient, 82 | final ListenerList fetchListenerList, 83 | final String apiBaseUrl, 84 | final String token, 85 | final long lastEventId, 86 | final long pollTime 87 | ) { 88 | this( 89 | new BotApi(gson, httpClient, apiBaseUrl + "/bot/v1/", token), 90 | new BackoffFetcher( 91 | httpClient, 92 | events -> fetchListenerList.asListener().onEventFetch(events), 93 | apiBaseUrl + "/bot/v1/events/get?token=" + token 94 | ), 95 | fetchListenerList, 96 | lastEventId, 97 | pollTime 98 | ); 99 | } 100 | 101 | public BotApiClient( 102 | final Api api, 103 | final Fetcher fetcher, 104 | final ListenerList fetchListenerList, 105 | final long lastEventId, 106 | final long pollTime 107 | ) { 108 | this.api = api; 109 | this.fetcher = fetcher; 110 | this.fetchListenerList = fetchListenerList; 111 | this.lastEventId = lastEventId; 112 | this.pollTime = pollTime; 113 | } 114 | 115 | public void start() { 116 | if (started.compareAndSet(false, true)) { 117 | startInternal(); 118 | BotLogger.i("bot successfully started"); 119 | } else { 120 | BotLogger.i("bot already started"); 121 | } 122 | } 123 | 124 | public void stop() { 125 | if (started.compareAndSet(true, false)) { 126 | stopInternal(); 127 | BotLogger.i("bot successfully stopped"); 128 | } else { 129 | BotLogger.i("bot is not running"); 130 | } 131 | } 132 | 133 | public ListenerDescriptor addOnEventFetchListener(final OnEventFetchListener listener) { 134 | return fetchListenerList.add(listener); 135 | } 136 | 137 | public Messages messages() { 138 | return api.messages(); 139 | } 140 | 141 | public Self self() { 142 | return api.self(); 143 | } 144 | 145 | public Chats chats() { 146 | return api.chats(); 147 | } 148 | 149 | private void startInternal() { 150 | fetcher.start(lastEventId, pollTime); 151 | } 152 | 153 | private void stopInternal() { 154 | fetcher.stop(); 155 | } 156 | 157 | } 158 | -------------------------------------------------------------------------------- /library/src/main/java/ru/mail/im/botapi/BotApiClientController.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi; 2 | 3 | import ru.mail.im.botapi.api.entity.AnswerCallbackQueryRequest; 4 | import ru.mail.im.botapi.api.entity.DeleteMessagesRequest; 5 | import ru.mail.im.botapi.api.entity.EditTextRequest; 6 | import ru.mail.im.botapi.api.entity.SendFileRequest; 7 | import ru.mail.im.botapi.api.entity.SendTextRequest; 8 | import ru.mail.im.botapi.entity.ChatAction; 9 | import ru.mail.im.botapi.response.ApiResponse; 10 | import ru.mail.im.botapi.response.ChatsGetAdminsResponse; 11 | import ru.mail.im.botapi.response.ChatsGetInfoResponse; 12 | import ru.mail.im.botapi.response.MessageResponse; 13 | import ru.mail.im.botapi.response.SelfGetResponse; 14 | 15 | import java.io.File; 16 | import java.io.IOException; 17 | import java.util.List; 18 | 19 | public class BotApiClientController { 20 | 21 | private final BotApiClient client; 22 | 23 | public static BotApiClientController startBot(BotApiClient client) { 24 | return new BotApiClientController(client); 25 | } 26 | 27 | private BotApiClientController(BotApiClient client) { 28 | this.client = client; 29 | client.start(); 30 | } 31 | 32 | public MessageResponse sendTextMessage(final SendTextRequest request) throws IOException { 33 | return client.messages().sendText( 34 | request.getChatId(), 35 | request.getText(), 36 | request.getFormat(), 37 | request.getParseMode(), 38 | toLongArray(request.getReplyMsgId()), 39 | request.getForwardChatId(), 40 | toLongArray(request.getForwardMsgId()), 41 | request.getKeyboard() 42 | ); 43 | } 44 | 45 | /** 46 | * @deprecated Use {@link BotApiClientController#sendTextMessage(SendTextRequest)} 47 | */ 48 | @Deprecated 49 | public MessageResponse sendTextMessage( 50 | final String chatId, 51 | final String text 52 | ) throws IOException { 53 | return client.messages().sendText(chatId, text, null, null, null, null, null, null); 54 | } 55 | 56 | /** 57 | * @deprecated Use {@link BotApiClientController#sendTextMessage(SendTextRequest)} 58 | */ 59 | @Deprecated 60 | public MessageResponse replyText( 61 | final String chatId, 62 | final String text, 63 | final long replyMsgId 64 | ) throws IOException { 65 | return client.messages().sendText(chatId, text, null, null, new long[]{replyMsgId}, null, null, null); 66 | } 67 | 68 | /** 69 | * @deprecated Use {@link BotApiClientController#sendTextMessage(SendTextRequest)} 70 | */ 71 | @Deprecated 72 | public MessageResponse forwardText( 73 | final String chatId, 74 | final String text, 75 | final String forwardChatId, 76 | final long forwardMsgId 77 | ) throws IOException { 78 | return client.messages().sendText(chatId, text, null, null, null, forwardChatId, new long[]{forwardMsgId}, null); 79 | } 80 | 81 | public MessageResponse sendFile(final SendFileRequest request) throws IOException { 82 | return client.messages().sendFile( 83 | request.getChatId(), 84 | request.getFile(), 85 | request.getCaption(), 86 | toLongArray(request.getReplyMsgId()), 87 | request.getForwardChatId(), 88 | toLongArray(request.getForwardMsgId()), 89 | request.getKeyboard() 90 | ); 91 | } 92 | 93 | /** 94 | * @deprecated Use {@link BotApiClientController#sendFile(SendFileRequest)} 95 | */ 96 | @Deprecated 97 | public MessageResponse sendFile( 98 | final String chatId, 99 | final File file 100 | ) throws IOException { 101 | return client.messages().sendFile(chatId, file, null, null, null, null, null); 102 | } 103 | 104 | /** 105 | * @deprecated Use {@link BotApiClientController#sendFile(SendFileRequest)} 106 | */ 107 | @Deprecated 108 | public MessageResponse sendFile( 109 | final String chatId, 110 | final File file, 111 | final String caption 112 | ) throws IOException { 113 | return client.messages().sendFile(chatId, file, caption, null, null, null, null); 114 | } 115 | 116 | /** 117 | * @deprecated Use {@link BotApiClientController#sendFile(SendFileRequest)} 118 | */ 119 | @Deprecated 120 | public MessageResponse replyFile( 121 | final String chatId, 122 | final File file, 123 | final long replyMsgId, 124 | final String caption 125 | ) throws IOException { 126 | return client.messages().sendFile(chatId, file, caption, new long[]{replyMsgId}, null, null, null); 127 | } 128 | 129 | /** 130 | * @deprecated Use {@link BotApiClientController#sendFile(SendFileRequest)} 131 | */ 132 | @Deprecated 133 | public MessageResponse forwardFile( 134 | final String chatId, 135 | final File file, 136 | final String forwardChatId, 137 | final long forwardMsgId, 138 | final String caption 139 | ) throws IOException { 140 | return client.messages().sendFile(chatId, file, caption, null, forwardChatId, new long[]{forwardMsgId}, null); 141 | } 142 | 143 | public ApiResponse editText(final EditTextRequest request) throws IOException { 144 | return client.messages().editText( 145 | request.getChatId(), 146 | request.getMsgId(), 147 | request.getNewText(), 148 | request.getKeyboard() 149 | ); 150 | } 151 | 152 | /** 153 | * @deprecated Use {@link BotApiClientController#editText(EditTextRequest)} 154 | */ 155 | @Deprecated 156 | public ApiResponse editText( 157 | final String chatId, 158 | final long msgId, 159 | final String text 160 | ) throws IOException { 161 | return client.messages().editText(chatId, msgId, text, null); 162 | } 163 | 164 | public ApiResponse deleteMessage(final DeleteMessagesRequest request) throws IOException { 165 | return client.messages().deleteMessages( 166 | request.getChatId(), 167 | toLongArray(request.getMsgId()) 168 | ); 169 | } 170 | 171 | /** 172 | * @deprecated Use {@link BotApiClientController#deleteMessage(DeleteMessagesRequest)} 173 | */ 174 | @Deprecated 175 | public ApiResponse deleteMessage( 176 | final String chatId, 177 | final long msgId 178 | ) throws IOException { 179 | return client.messages().deleteMessages(chatId, new long[] {msgId}); 180 | } 181 | 182 | public ApiResponse answerCallbackQuery(final AnswerCallbackQueryRequest request) throws IOException { 183 | return client.messages().answerCallbackQuery( 184 | request.getQueryId(), 185 | request.getText(), 186 | request.getShowAlert(), 187 | request.getUrl() 188 | ); 189 | } 190 | 191 | public ChatsGetAdminsResponse getChatAdmins(String chatId) throws IOException { 192 | return client.chats().getAdmins(chatId); 193 | } 194 | 195 | public ChatsGetInfoResponse getChatInfo(String chatId) throws IOException { 196 | return client.chats().getInfo(chatId); 197 | } 198 | 199 | public ApiResponse sendActions(String chatId, ChatAction... actions) throws IOException { 200 | return client.chats().sendActions(chatId, actions); 201 | } 202 | 203 | public SelfGetResponse getSelfInfo() throws IOException { 204 | return client.self().get(); 205 | } 206 | 207 | private long[] toLongArray(final List longList) { 208 | if (longList == null) { 209 | return null; 210 | } else { 211 | return longList.stream().mapToLong(msgId -> msgId).toArray(); 212 | } 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /library/src/main/java/ru/mail/im/botapi/BotLogger.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | public class BotLogger { 7 | private static Logger logger = LoggerFactory.getLogger(BotLogger.class); 8 | private static String BOT_TAG = "IM_BOT"; 9 | 10 | public static void i(String message) { 11 | logger.info(String.format("%s %s", BOT_TAG, message)); 12 | } 13 | 14 | public static void e(Exception e) { 15 | logger.error(e.getMessage(), String.format("%s %s", BOT_TAG, e.getMessage())); 16 | } 17 | 18 | public static void e(Exception e, String message) { 19 | logger.error(String.format("%s %s", BOT_TAG, message), e); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /library/src/main/java/ru/mail/im/botapi/api/Api.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.api; 2 | 3 | public interface Api { 4 | Self self(); 5 | Messages messages(); 6 | Chats chats(); 7 | } 8 | -------------------------------------------------------------------------------- /library/src/main/java/ru/mail/im/botapi/api/ApiImplementationFactory.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.api; 2 | 3 | import com.google.gson.Gson; 4 | import okhttp3.HttpUrl; 5 | import okhttp3.MultipartBody; 6 | import okhttp3.Request; 7 | import okhttp3.RequestBody; 8 | 9 | import javax.annotation.Nonnull; 10 | import java.io.File; 11 | import java.io.IOException; 12 | import java.lang.reflect.Method; 13 | import java.lang.reflect.Proxy; 14 | import java.nio.file.Files; 15 | import java.util.List; 16 | 17 | class ApiImplementationFactory { 18 | 19 | private final Gson gson; 20 | private final RequestExecutor requestExecutor; 21 | private final HttpUrl baseUrl; 22 | private final String token; 23 | 24 | ApiImplementationFactory( 25 | @Nonnull final Gson gson, 26 | @Nonnull final RequestExecutor requestExecutor, 27 | @Nonnull final String baseUrl, 28 | @Nonnull final String token 29 | ) { 30 | this.gson = gson; 31 | this.requestExecutor = requestExecutor; 32 | this.baseUrl = HttpUrl.get(baseUrl); 33 | this.token = token; 34 | } 35 | 36 | T createImplementation(final Class clazz) { 37 | Object impl = Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz}, (proxy, method, args) -> { 38 | final List query = new QueryBuilder(gson, method, args).build(); 39 | final Request request = buildRequest(method, query); 40 | return requestExecutor.execute(request, method.getReturnType()); 41 | }); 42 | return clazz.cast(impl); 43 | } 44 | 45 | private Request buildRequest(final Method method, final List query) throws IOException { 46 | verifyMethod(method); 47 | GetRequest getAnnotation = method.getAnnotation(GetRequest.class); 48 | if (getAnnotation != null) { 49 | return buildGetRequest(getAnnotation.value(), query); 50 | } 51 | 52 | PostRequest postAnnotation = method.getAnnotation(PostRequest.class); 53 | if (postAnnotation != null) { 54 | return buildPostRequest(postAnnotation.value(), query); 55 | } 56 | throw new IllegalArgumentException("Request must be GET or POST"); 57 | } 58 | 59 | private Request buildGetRequest(final String name, final List query) { 60 | final HttpUrl.Builder urlBuilder = baseUrl.newBuilder() 61 | .addPathSegments(name) 62 | .addQueryParameter("token", token); 63 | for (QueryParameter parameter : query) { 64 | urlBuilder.addQueryParameter(parameter.name, parameter.getValueAsString()); 65 | } 66 | return new Request.Builder() 67 | .url(urlBuilder.build()) 68 | .build(); 69 | } 70 | 71 | private Request buildPostRequest(final String name, final List query) throws IOException { 72 | final HttpUrl url = baseUrl.newBuilder() 73 | .addPathSegments(name) 74 | .build(); 75 | final MultipartBody.Builder builder = new MultipartBody.Builder() 76 | .setType(MultipartBody.FORM) 77 | .addFormDataPart("token", token); 78 | for (QueryParameter parameter : query) { 79 | if (parameter.value instanceof File) { 80 | File file = (File) parameter.value; 81 | builder.addFormDataPart(parameter.name, file.getName(), RequestBody.create(null, Files.readAllBytes(file.toPath()))); 82 | } else if (parameter.value != null) { 83 | builder.addFormDataPart(parameter.name, parameter.value.toString()); 84 | } 85 | } 86 | return new Request.Builder() 87 | .url(url) 88 | .post(builder.build()) 89 | .build(); 90 | } 91 | 92 | private static void verifyMethod(final Method method) { 93 | for (Class exClass : method.getExceptionTypes()) { 94 | if (exClass == IOException.class) { 95 | return; 96 | } 97 | } 98 | throw new RuntimeException("Request method must throw IOException"); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /library/src/main/java/ru/mail/im/botapi/api/BotApi.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.api; 2 | 3 | import com.google.gson.Gson; 4 | import okhttp3.OkHttpClient; 5 | 6 | import javax.annotation.Nonnull; 7 | 8 | public class BotApi implements Api { 9 | 10 | private final Messages messages; 11 | private final Self self; 12 | private final Chats chats; 13 | 14 | public BotApi( 15 | @Nonnull final Gson gson, 16 | @Nonnull final OkHttpClient httpClient, 17 | @Nonnull final String baseUrl, 18 | @Nonnull final String token 19 | ) { 20 | final ApiImplementationFactory factory = new ApiImplementationFactory( 21 | gson, 22 | new OkHttpRequestExecutor(httpClient), 23 | baseUrl, 24 | token 25 | ); 26 | 27 | messages = factory.createImplementation(Messages.class); 28 | self = factory.createImplementation(Self.class); 29 | chats = factory.createImplementation(Chats.class); 30 | } 31 | 32 | @Override 33 | public Self self() { 34 | return self; 35 | } 36 | 37 | @Override 38 | public Messages messages() { 39 | return messages; 40 | } 41 | 42 | @Override 43 | public Chats chats() { 44 | return chats; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /library/src/main/java/ru/mail/im/botapi/api/Chats.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.api; 2 | 3 | import ru.mail.im.botapi.entity.ChatAction; 4 | import ru.mail.im.botapi.response.ApiResponse; 5 | import ru.mail.im.botapi.response.ChatsGetAdminsResponse; 6 | import ru.mail.im.botapi.response.ChatsGetInfoResponse; 7 | 8 | import java.io.IOException; 9 | 10 | public interface Chats { 11 | 12 | @GetRequest("chats/sendActions") 13 | ApiResponse sendActions(@RequestParam("chatId") final String chatId, 14 | @RequestParam("actions") final ChatAction... actions) throws IOException; 15 | 16 | @GetRequest("chats/getInfo") 17 | ChatsGetInfoResponse getInfo(@RequestParam("chatId") final String chatId) throws IOException; 18 | 19 | @GetRequest("chats/getAdmins") 20 | ChatsGetAdminsResponse getAdmins(@RequestParam("chatId") final String chatId) throws IOException; 21 | } 22 | -------------------------------------------------------------------------------- /library/src/main/java/ru/mail/im/botapi/api/Events.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.api; 2 | 3 | import ru.mail.im.botapi.response.ApiResponse; 4 | 5 | import java.io.IOException; 6 | 7 | public interface Events { 8 | 9 | @GetRequest("events/get") 10 | ApiResponse fetchEvents() throws IOException; 11 | } 12 | -------------------------------------------------------------------------------- /library/src/main/java/ru/mail/im/botapi/api/GetRequest.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.api; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Retention(RetentionPolicy.RUNTIME) 9 | @Target(ElementType.METHOD) 10 | @interface GetRequest { 11 | String value(); 12 | } 13 | -------------------------------------------------------------------------------- /library/src/main/java/ru/mail/im/botapi/api/Messages.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.api; 2 | 3 | import ru.mail.im.botapi.api.entity.InlineKeyboardButton; 4 | import ru.mail.im.botapi.response.ApiResponse; 5 | import ru.mail.im.botapi.response.MessageResponse; 6 | 7 | import java.io.File; 8 | import java.io.IOException; 9 | import java.util.List; 10 | 11 | public interface Messages { 12 | 13 | @GetRequest("messages/sendText") 14 | MessageResponse sendText( 15 | @RequestParam("chatId") final String chatId, 16 | @RequestParam("text") final String text, 17 | @RequestParam("format") final String format, 18 | @RequestParam("parseMode") final String parseMode, 19 | @RequestParam("replyMsgId") final long[] replyMsgId, 20 | @RequestParam("forwardChatId") final String forwardChatId, 21 | @RequestParam("forwardMsgId") final long[] forwardMsgId, 22 | @RequestParam("inlineKeyboardMarkup") final List> keyboard 23 | ) throws IOException; 24 | 25 | @PostRequest("messages/sendFile") 26 | MessageResponse sendFile( 27 | @RequestParam("chatId") final String chatId, 28 | @RequestParam("file") final File file, 29 | @RequestParam("caption") String caption, 30 | @RequestParam("replyMsgId") final long[] replyMsgId, 31 | @RequestParam("forwardChatId") final String forwardChatId, 32 | @RequestParam("forwardMsgId") final long[] forwardMsgId, 33 | @RequestParam("inlineKeyboardMarkup") final List> keyboard 34 | ) throws IOException; 35 | 36 | @GetRequest("messages/editText") 37 | ApiResponse editText( 38 | @RequestParam("chatId") final String chatId, 39 | @RequestParam("msgId") final long msgId, 40 | @RequestParam("text") final String newText, 41 | @RequestParam("inlineKeyboardMarkup") final List> keyboard 42 | ) throws IOException; 43 | 44 | 45 | @GetRequest("messages/deleteMessages") 46 | ApiResponse deleteMessages( 47 | @RequestParam("chatId") final String chatId, 48 | @RequestParam("msgId") final long[] msgId 49 | ) throws IOException; 50 | 51 | @GetRequest("messages/answerCallbackQuery") 52 | ApiResponse answerCallbackQuery( 53 | @RequestParam("queryId") final String queryId, 54 | @RequestParam("text") final String text, 55 | @RequestParam("showAlert") final Boolean showAlert, 56 | @RequestParam("url") final String url 57 | ) throws IOException; 58 | 59 | } 60 | -------------------------------------------------------------------------------- /library/src/main/java/ru/mail/im/botapi/api/OkHttpRequestExecutor.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.api; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.GsonBuilder; 5 | import okhttp3.OkHttpClient; 6 | import okhttp3.Request; 7 | import okhttp3.Response; 8 | import okhttp3.ResponseBody; 9 | import ru.mail.im.botapi.BotLogger; 10 | import ru.mail.im.botapi.entity.ChatType; 11 | import ru.mail.im.botapi.json.ChatTypeGsonDeserializer; 12 | 13 | import java.io.IOException; 14 | 15 | class OkHttpRequestExecutor implements RequestExecutor { 16 | 17 | private final Gson gson = new GsonBuilder() 18 | .registerTypeAdapter(ChatType.class, new ChatTypeGsonDeserializer()) 19 | .create(); 20 | 21 | private OkHttpClient httpClient; 22 | 23 | OkHttpRequestExecutor(final OkHttpClient httpClient) { 24 | this.httpClient = httpClient; 25 | } 26 | 27 | @Override 28 | public T execute(final Request request, final Class responseClass) throws IOException { 29 | BotLogger.i("request:" + request.url()); 30 | try (Response response = httpClient.newCall(request).execute()) { 31 | String message = response.message(); 32 | BotLogger.i("response message:" + message); 33 | if (!response.isSuccessful()) { 34 | throw new IOException("Bad HTTP status " + response.code()); 35 | } 36 | final ResponseBody body = response.body(); 37 | if (body == null) { 38 | throw new NullPointerException("Response body is null"); 39 | } 40 | String json = body.string() ; 41 | BotLogger.i("response body:" + json); 42 | return gson.fromJson(json, responseClass); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /library/src/main/java/ru/mail/im/botapi/api/PostRequest.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.api; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Retention(RetentionPolicy.RUNTIME) 9 | @Target(ElementType.METHOD) 10 | @interface PostRequest { 11 | String value(); 12 | } 13 | -------------------------------------------------------------------------------- /library/src/main/java/ru/mail/im/botapi/api/QueryBuilder.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.api; 2 | 3 | import com.google.gson.Gson; 4 | 5 | import java.lang.annotation.Annotation; 6 | import java.lang.reflect.Array; 7 | import java.lang.reflect.Method; 8 | import java.util.ArrayList; 9 | import java.util.Collections; 10 | import java.util.List; 11 | 12 | class QueryBuilder { 13 | private final Gson gson; 14 | private final Method method; 15 | private final Object[] args; 16 | 17 | QueryBuilder(final Gson gson, final Method method, final Object[] args) { 18 | this.gson = gson; 19 | this.method = method; 20 | this.args = args; 21 | } 22 | 23 | List build() { 24 | if (args == null) { 25 | return Collections.emptyList(); 26 | } 27 | final List result = new ArrayList<>(args.length); 28 | for (int i = 0; i < args.length; i++) { 29 | final String name = getParamName(i); 30 | for (Object value : getParamValues(i)) { 31 | if (value != null) { 32 | result.add(new QueryParameter(gson, name, value)); 33 | } 34 | } 35 | } 36 | return result; 37 | } 38 | 39 | private String getParamName(final int index) { 40 | final Annotation[] annotations = method.getParameterAnnotations()[index]; 41 | for (Annotation annotation : annotations) { 42 | if (annotation instanceof RequestParam) { 43 | return ((RequestParam) annotation).value(); 44 | } 45 | } 46 | throw new IllegalArgumentException("Parameter with index " + index + " was not annotated with @RequestParam"); 47 | } 48 | 49 | private List getParamValues(final int index) { 50 | Object arg = args[index]; 51 | if (arg != null && arg.getClass().isArray()) { 52 | final int length = Array.getLength(arg); 53 | final List list = new ArrayList<>(length); 54 | for (int i = 0; i < length; i++) { 55 | list.add(Array.get(arg, i)); 56 | } 57 | return list; 58 | } 59 | return Collections.singletonList(arg); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /library/src/main/java/ru/mail/im/botapi/api/QueryParameter.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.api; 2 | 3 | import com.google.gson.Gson; 4 | 5 | import javax.annotation.Nullable; 6 | 7 | class QueryParameter { 8 | final Gson gson; 9 | final String name; 10 | final Object value; 11 | 12 | QueryParameter(final Gson gson, final String name, final Object value) { 13 | this.gson = gson; 14 | this.name = name; 15 | this.value = value; 16 | } 17 | 18 | @Nullable 19 | String getValueAsString() { 20 | if (value != null) { 21 | if (value instanceof String) { 22 | return value.toString(); 23 | } else { 24 | return gson.toJson(value); 25 | } 26 | } else { 27 | return null; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /library/src/main/java/ru/mail/im/botapi/api/RequestExecutor.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.api; 2 | 3 | import okhttp3.Request; 4 | 5 | import java.io.IOException; 6 | 7 | interface RequestExecutor { 8 | T execute(Request request, Class responseClass) throws IOException; 9 | } 10 | -------------------------------------------------------------------------------- /library/src/main/java/ru/mail/im/botapi/api/RequestParam.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.api; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Retention(RetentionPolicy.RUNTIME) 9 | @Target(ElementType.PARAMETER) 10 | @interface RequestParam { 11 | String value(); 12 | } 13 | -------------------------------------------------------------------------------- /library/src/main/java/ru/mail/im/botapi/api/Self.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.api; 2 | 3 | import ru.mail.im.botapi.response.SelfGetResponse; 4 | 5 | import java.io.IOException; 6 | 7 | public interface Self { 8 | 9 | @GetRequest("self/get") 10 | SelfGetResponse get() throws IOException; 11 | } 12 | -------------------------------------------------------------------------------- /library/src/main/java/ru/mail/im/botapi/api/entity/AnswerCallbackQueryRequest.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.api.entity; 2 | 3 | public class AnswerCallbackQueryRequest { 4 | private String queryId; 5 | private String text; 6 | private Boolean showAlert; 7 | private String url; 8 | 9 | public static AnswerCallbackQueryRequest answerText(final String queryId, final String text, final boolean showAlert) { 10 | return new AnswerCallbackQueryRequest() 11 | .setQueryId(queryId) 12 | .setText(text) 13 | .setShowAlert(showAlert); 14 | } 15 | 16 | public static AnswerCallbackQueryRequest answerUrl(final String queryId, final String text, final String url) { 17 | return new AnswerCallbackQueryRequest() 18 | .setQueryId(queryId) 19 | .setText(text) 20 | .setUrl(url); 21 | } 22 | 23 | public String getQueryId() { 24 | return queryId; 25 | } 26 | 27 | public AnswerCallbackQueryRequest setQueryId(final String queryId) { 28 | this.queryId = queryId; 29 | return this; 30 | } 31 | 32 | public String getText() { 33 | return text; 34 | } 35 | 36 | public AnswerCallbackQueryRequest setText(final String text) { 37 | this.text = text; 38 | return this; 39 | } 40 | 41 | public Boolean getShowAlert() { 42 | return showAlert; 43 | } 44 | 45 | public AnswerCallbackQueryRequest setShowAlert(final Boolean showAlert) { 46 | this.showAlert = showAlert; 47 | return this; 48 | } 49 | 50 | public String getUrl() { 51 | return url; 52 | } 53 | 54 | public AnswerCallbackQueryRequest setUrl(final String url) { 55 | this.url = url; 56 | return this; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /library/src/main/java/ru/mail/im/botapi/api/entity/DeleteMessagesRequest.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.api.entity; 2 | 3 | import java.util.List; 4 | 5 | public class DeleteMessagesRequest { 6 | private String chatId; 7 | private List msgId; 8 | 9 | public String getChatId() { 10 | return chatId; 11 | } 12 | 13 | public DeleteMessagesRequest setChatId(final String chatId) { 14 | this.chatId = chatId; 15 | return this; 16 | } 17 | 18 | public List getMsgId() { 19 | return msgId; 20 | } 21 | 22 | public DeleteMessagesRequest setMsgId(final List msgId) { 23 | this.msgId = msgId; 24 | return this; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /library/src/main/java/ru/mail/im/botapi/api/entity/EditTextRequest.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.api.entity; 2 | 3 | import java.util.List; 4 | 5 | public class EditTextRequest { 6 | private String chatId; 7 | private long msgId; 8 | private String newText; 9 | private List> keyboard; 10 | 11 | public String getChatId() { 12 | return chatId; 13 | } 14 | 15 | public EditTextRequest setChatId(final String chatId) { 16 | this.chatId = chatId; 17 | return this; 18 | } 19 | 20 | public long getMsgId() { 21 | return msgId; 22 | } 23 | 24 | public EditTextRequest setMsgId(final long msgId) { 25 | this.msgId = msgId; 26 | return this; 27 | } 28 | 29 | public String getNewText() { 30 | return newText; 31 | } 32 | 33 | public EditTextRequest setNewText(final String newText) { 34 | this.newText = newText; 35 | return this; 36 | } 37 | 38 | public List> getKeyboard() { 39 | return keyboard; 40 | } 41 | 42 | public EditTextRequest setKeyboard(final List> keyboard) { 43 | this.keyboard = keyboard; 44 | return this; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /library/src/main/java/ru/mail/im/botapi/api/entity/InlineKeyboardButton.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.api.entity; 2 | 3 | public class InlineKeyboardButton { 4 | 5 | private final String text; 6 | private final String url; 7 | private final String callbackData; 8 | private final String style; 9 | 10 | private InlineKeyboardButton(final String text, final String url, final String callbackData, final String style) { 11 | this.text = text; 12 | this.url = url; 13 | this.callbackData = callbackData; 14 | this.style = style; 15 | } 16 | 17 | public static InlineKeyboardButton callbackButton(final String text, final String callbackData, final String style) { 18 | return new InlineKeyboardButton(text, null, callbackData, style); 19 | } 20 | 21 | public static InlineKeyboardButton urlButton(final String text, final String url, final String style) { 22 | return new InlineKeyboardButton(text, url, null, style); 23 | } 24 | 25 | public String getText() { 26 | return text; 27 | } 28 | 29 | public String getUrl() { 30 | return url; 31 | } 32 | 33 | public String getCallbackData() { 34 | return callbackData; 35 | } 36 | 37 | public String getStyle() { 38 | return style; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /library/src/main/java/ru/mail/im/botapi/api/entity/SendFileRequest.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.api.entity; 2 | 3 | import java.io.File; 4 | import java.util.List; 5 | 6 | public class SendFileRequest { 7 | private String chatId; 8 | private File file; 9 | private String caption; 10 | private List replyMsgId; 11 | private String forwardChatId; 12 | private List forwardMsgId; 13 | private List> keyboard; 14 | 15 | public String getChatId() { 16 | return chatId; 17 | } 18 | 19 | public SendFileRequest setChatId(final String chatId) { 20 | this.chatId = chatId; 21 | return this; 22 | } 23 | 24 | public File getFile() { 25 | return file; 26 | } 27 | 28 | public SendFileRequest setFile(final File file) { 29 | this.file = file; 30 | return this; 31 | } 32 | 33 | public String getCaption() { 34 | return caption; 35 | } 36 | 37 | public SendFileRequest setCaption(final String caption) { 38 | this.caption = caption; 39 | return this; 40 | } 41 | 42 | public List getReplyMsgId() { 43 | return replyMsgId; 44 | } 45 | 46 | public SendFileRequest setReplyMsgId(final List replyMsgId) { 47 | this.replyMsgId = replyMsgId; 48 | return this; 49 | } 50 | 51 | public String getForwardChatId() { 52 | return forwardChatId; 53 | } 54 | 55 | public SendFileRequest setForwardChatId(final String forwardChatId) { 56 | this.forwardChatId = forwardChatId; 57 | return this; 58 | } 59 | 60 | public List getForwardMsgId() { 61 | return forwardMsgId; 62 | } 63 | 64 | public SendFileRequest setForwardMsgId(final List forwardMsgId) { 65 | this.forwardMsgId = forwardMsgId; 66 | return this; 67 | } 68 | 69 | public List> getKeyboard() { 70 | return keyboard; 71 | } 72 | 73 | public SendFileRequest setKeyboard(final List> keyboard) { 74 | this.keyboard = keyboard; 75 | return this; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /library/src/main/java/ru/mail/im/botapi/api/entity/SendTextRequest.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.api.entity; 2 | 3 | import java.util.List; 4 | 5 | public class SendTextRequest { 6 | private String chatId; 7 | private String text; 8 | private String format; 9 | private String parseMode; 10 | private List replyMsgId; 11 | private String forwardChatId; 12 | private List forwardMsgId; 13 | private List> keyboard; 14 | 15 | public String getChatId() { 16 | return chatId; 17 | } 18 | 19 | public SendTextRequest setChatId(final String chatId) { 20 | this.chatId = chatId; 21 | return this; 22 | } 23 | 24 | public String getText() { 25 | return text; 26 | } 27 | 28 | public SendTextRequest setText(final String text) { 29 | this.text = text; 30 | return this; 31 | } 32 | 33 | public List getReplyMsgId() { 34 | return replyMsgId; 35 | } 36 | 37 | public SendTextRequest setReplyMsgId(final List replyMsgId) { 38 | this.replyMsgId = replyMsgId; 39 | return this; 40 | } 41 | 42 | public String getForwardChatId() { 43 | return forwardChatId; 44 | } 45 | 46 | public SendTextRequest setForwardChatId(final String forwardChatId) { 47 | this.forwardChatId = forwardChatId; 48 | return this; 49 | } 50 | 51 | public List getForwardMsgId() { 52 | return forwardMsgId; 53 | } 54 | 55 | public SendTextRequest setForwardMsgId(final List forwardMsgId) { 56 | this.forwardMsgId = forwardMsgId; 57 | return this; 58 | } 59 | 60 | public List> getKeyboard() { 61 | return keyboard; 62 | } 63 | 64 | public SendTextRequest setKeyboard(final List> keyboard) { 65 | this.keyboard = keyboard; 66 | return this; 67 | } 68 | 69 | public String getFormat() { 70 | return format; 71 | } 72 | 73 | public SendTextRequest setFormat(String format) { 74 | this.format = format; 75 | return this; 76 | } 77 | 78 | public String getParseMode() { 79 | return parseMode; 80 | } 81 | 82 | public SendTextRequest setParseMode(String parseMode) { 83 | this.parseMode = parseMode; 84 | return this; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /library/src/main/java/ru/mail/im/botapi/entity/Admin.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.entity; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | 5 | public class Admin { 6 | 7 | @SerializedName("userId") 8 | private String userId; 9 | 10 | @SerializedName("creator") 11 | private boolean creator; 12 | 13 | public String getUserId() { 14 | return userId; 15 | } 16 | 17 | public boolean isCreator() { 18 | return creator; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /library/src/main/java/ru/mail/im/botapi/entity/Buddy.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.entity; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | 5 | public class Buddy { 6 | 7 | @SerializedName("aimId") 8 | private String id; 9 | 10 | @SerializedName("friendly") 11 | private String name; 12 | 13 | // TODO replace with Enum if the field will remains in new fetch protocol 14 | @SerializedName("userType") 15 | private String userType; 16 | 17 | public String getId() { 18 | return id; 19 | } 20 | 21 | public String getName() { 22 | return name; 23 | } 24 | 25 | public String getUserType() { 26 | return userType; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /library/src/main/java/ru/mail/im/botapi/entity/ChatAction.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.entity; 2 | 3 | public enum ChatAction { 4 | LOOKING("looking"), 5 | TYPING("typing"), 6 | NONE(""); 7 | 8 | private String value; 9 | 10 | ChatAction(final String value) { 11 | this.value = value; 12 | } 13 | 14 | @Override 15 | public String toString() { 16 | return value; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /library/src/main/java/ru/mail/im/botapi/entity/ChatType.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.entity; 2 | 3 | import javax.annotation.Nullable; 4 | 5 | public enum ChatType { 6 | PRIVATE("private"), 7 | GROUP("group"), 8 | CHANNEL("channel"); 9 | 10 | private String apiValue; 11 | 12 | ChatType(final String apiValue) { 13 | this.apiValue = apiValue; 14 | } 15 | 16 | public String getApiValue() { 17 | return apiValue; 18 | } 19 | 20 | @Nullable 21 | public static ChatType fromApiValue(final String value) { 22 | for (ChatType type : values()) { 23 | if (type.apiValue.equals(value)) { 24 | return type; 25 | } 26 | } 27 | return null; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /library/src/main/java/ru/mail/im/botapi/entity/Photo.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.entity; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | 5 | public class Photo { 6 | 7 | @SerializedName("url") 8 | private String url; 9 | 10 | public String getUrl() { 11 | return url; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /library/src/main/java/ru/mail/im/botapi/fetcher/BackoffFetcher.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.fetcher; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.GsonBuilder; 5 | import okhttp3.OkHttpClient; 6 | import okhttp3.Request; 7 | import okhttp3.Response; 8 | import okhttp3.ResponseBody; 9 | import ru.mail.im.botapi.BotLogger; 10 | import ru.mail.im.botapi.fetcher.event.Event; 11 | import ru.mail.im.botapi.fetcher.event.parts.Part; 12 | import ru.mail.im.botapi.util.IOBackoff; 13 | 14 | import java.io.IOException; 15 | import java.util.List; 16 | import java.util.concurrent.ExecutorService; 17 | import java.util.concurrent.Executors; 18 | import java.util.concurrent.atomic.AtomicBoolean; 19 | 20 | public class BackoffFetcher implements Fetcher { 21 | 22 | private String baseUrl; 23 | 24 | private String nextUrl; 25 | 26 | private long pollTime; 27 | 28 | private long lastEventId; 29 | 30 | private OnEventFetchListener listener; 31 | 32 | private Gson gson; 33 | 34 | private final OkHttpClient client; 35 | 36 | private final AtomicBoolean isRunning = new AtomicBoolean(); 37 | 38 | private final IOBackoff backoff = IOBackoff.newBuilder() 39 | .startTime(100) 40 | .maxTime(10_000) 41 | .factor(2) 42 | .build(); 43 | 44 | private final ExecutorService executor; 45 | 46 | public BackoffFetcher(final OkHttpClient client, 47 | final OnEventFetchListener listener, 48 | final String baseUrl) { 49 | this(client, listener, baseUrl, Executors.newSingleThreadExecutor()); 50 | } 51 | 52 | public BackoffFetcher(final OkHttpClient client, 53 | final OnEventFetchListener listener, 54 | final String baseUrl, 55 | final ExecutorService executor) { 56 | this.client = client; 57 | this.listener = listener; 58 | this.baseUrl = baseUrl; 59 | this.executor = executor; 60 | } 61 | 62 | @Override 63 | public void start(final long lastEventId, final long pollTime) { 64 | this.lastEventId = lastEventId; 65 | this.pollTime = pollTime; 66 | 67 | if (isRunning.compareAndSet(false, true)) { 68 | gson = new GsonBuilder() 69 | .registerTypeAdapter(Event.class, new EventDeserializer()) 70 | .registerTypeAdapter(Part.class, new PartDeserializer()) 71 | .create(); 72 | executor.execute(() -> { 73 | nextUrl = getNextUrl(); 74 | while (isRunning.get()) { 75 | backoff.execute(this::fetchNext); 76 | } 77 | }); 78 | } 79 | } 80 | 81 | private String getNextUrl() { 82 | return String.format("%s&lastEventId=%d&pollTime=%d", baseUrl, lastEventId, pollTime); 83 | } 84 | 85 | @Override 86 | public void stop() { 87 | isRunning.set(false); 88 | executor.shutdown(); 89 | BotLogger.i("fetcher stopped"); 90 | } 91 | 92 | private void fetchNext() throws IOException { 93 | final Request httpRequest = new Request.Builder() 94 | .url(nextUrl) 95 | .build(); 96 | BotLogger.i("fetch next url:" + nextUrl); 97 | try (Response httpResponse = client.newCall(httpRequest).execute()) { 98 | if (!httpResponse.isSuccessful()) { 99 | throw new IOException("Bad HTTP status " + httpResponse.code()); 100 | } 101 | final ResponseBody body = httpResponse.body(); 102 | if (body == null) { 103 | throw new NullPointerException("Response body is null"); 104 | } 105 | String json = body.string(); 106 | BotLogger.i("new events fetched:" + json); 107 | final FetchResponse fetchResponse = gson.fromJson(json, FetchResponse.class); 108 | handleFetchResponse(fetchResponse); 109 | } 110 | } 111 | 112 | private void handleFetchResponse(final FetchResponse fetchResponse) { 113 | handleEvents(fetchResponse.getEvents()); 114 | 115 | if (fetchResponse.getEvents() != null && !fetchResponse.getEvents().isEmpty()) { 116 | int size = fetchResponse.getEvents().size(); 117 | lastEventId = fetchResponse.getEvents().get(size - 1).getEventId(); 118 | } 119 | 120 | this.nextUrl = getNextUrl(); 121 | } 122 | 123 | private void handleEvents(final List events) { 124 | if (isRunning.get()) { 125 | listener.onEventFetch(events); 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /library/src/main/java/ru/mail/im/botapi/fetcher/Chat.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.fetcher; 2 | 3 | public class Chat { 4 | private String chatId; 5 | private String title; 6 | private String type; 7 | 8 | public String getType() { 9 | return type; 10 | } 11 | 12 | public String getTitle() { 13 | return title; 14 | } 15 | 16 | public String getChatId() { 17 | return chatId; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /library/src/main/java/ru/mail/im/botapi/fetcher/EventDeserializer.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.fetcher; 2 | 3 | import com.google.gson.*; 4 | import ru.mail.im.botapi.fetcher.event.*; 5 | 6 | import javax.annotation.Nonnull; 7 | import java.lang.reflect.Type; 8 | 9 | class EventDeserializer implements JsonDeserializer { 10 | 11 | @Override 12 | public Event deserialize(final JsonElement json, 13 | final Type typeOfT, 14 | final JsonDeserializationContext context) throws JsonParseException { 15 | switch (extractEventType(json)) { 16 | case "newMessage": 17 | return context.deserialize(json, NewMessageEvent.class); 18 | case "editedMessage": 19 | return context.deserialize(json, EditedMessageEvent.class); 20 | case "deletedMessage": 21 | return context.deserialize(json, DeletedMessageEvent.class); 22 | case "pinnedMessage": 23 | return context.deserialize(json, PinnedMessageEvent.class); 24 | case "unpinnedMessage": 25 | return context.deserialize(json, UnpinnedMessageEvent.class); 26 | case "newChatMembers": 27 | return context.deserialize(json, NewChatMembersEvent.class); 28 | case "leftChatMembers": 29 | return context.deserialize(json, LeftChatMembersEvent.class); 30 | case "callbackQuery": 31 | return context.deserialize(json, CallbackQueryEvent.class); 32 | default: 33 | return new UnknownEvent(json.toString()); 34 | } 35 | } 36 | 37 | @Nonnull 38 | private static String extractEventType(final JsonElement json) { 39 | if (!json.isJsonObject()) { 40 | return ""; 41 | } 42 | JsonObject object = json.getAsJsonObject(); 43 | if (!object.has("type") || !object.get("type").isJsonPrimitive()) { 44 | return ""; 45 | } 46 | JsonPrimitive jsonType = object.getAsJsonPrimitive("type"); 47 | if (!jsonType.isString()) { 48 | return ""; 49 | } 50 | final String type = jsonType.getAsString(); 51 | return type == null ? "" : type; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /library/src/main/java/ru/mail/im/botapi/fetcher/FetchResponse.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.fetcher; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | import ru.mail.im.botapi.fetcher.event.Event; 5 | 6 | import java.util.List; 7 | 8 | class FetchResponse { 9 | 10 | @SerializedName("events") 11 | private List events; 12 | 13 | @SerializedName("ok") 14 | private boolean ok; 15 | 16 | public List getEvents() { 17 | return events; 18 | } 19 | 20 | public boolean isOk() { 21 | return ok; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /library/src/main/java/ru/mail/im/botapi/fetcher/Fetcher.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.fetcher; 2 | 3 | public interface Fetcher { 4 | void start(final long lastEventId, final long pollTime); 5 | void stop(); 6 | } 7 | -------------------------------------------------------------------------------- /library/src/main/java/ru/mail/im/botapi/fetcher/Message.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.fetcher; 2 | 3 | public class Message { 4 | 5 | private User from; 6 | private String msgId; 7 | private String text; 8 | private long timestamp; 9 | 10 | public User getFrom() { 11 | return from; 12 | } 13 | 14 | public String getMsgId() { 15 | return msgId; 16 | } 17 | 18 | public String getText() { 19 | return text; 20 | } 21 | 22 | public long getTimestamp() { 23 | return timestamp; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /library/src/main/java/ru/mail/im/botapi/fetcher/OnEventFetchListener.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.fetcher; 2 | 3 | import ru.mail.im.botapi.fetcher.event.Event; 4 | 5 | import java.util.List; 6 | 7 | public interface OnEventFetchListener { 8 | void onEventFetch(final List events); 9 | } 10 | -------------------------------------------------------------------------------- /library/src/main/java/ru/mail/im/botapi/fetcher/PartDeserializer.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.fetcher; 2 | 3 | import com.google.gson.*; 4 | import ru.mail.im.botapi.fetcher.event.parts.*; 5 | 6 | import javax.annotation.Nonnull; 7 | import java.lang.reflect.Type; 8 | 9 | public class PartDeserializer implements JsonDeserializer { 10 | @Override 11 | public Part deserialize(JsonElement json, 12 | Type typeOfT, 13 | JsonDeserializationContext context) throws JsonParseException { 14 | switch (extractPartType(json)) { 15 | case "sticker": 16 | return context.deserialize(extractPayload(json), Sticker.class); 17 | case "mention": 18 | return context.deserialize(extractPayload(json), Mention.class); 19 | case "voice": 20 | return context.deserialize(extractPayload(json), Voice.class); 21 | case "file": 22 | return context.deserialize(extractPayload(json), File.class); 23 | case "forward": 24 | return context.deserialize(extractPayload(json), Forward.class); 25 | case "reply": 26 | return context.deserialize(extractPayload(json), Reply.class); 27 | } 28 | return null; 29 | } 30 | 31 | @Nonnull 32 | private static String extractPartType(final JsonElement json) { 33 | if (!json.isJsonObject()) { 34 | return ""; 35 | } 36 | JsonObject object = json.getAsJsonObject(); 37 | if (!object.has("type") || !object.get("type").isJsonPrimitive()) { 38 | return ""; 39 | } 40 | JsonPrimitive jsonType = object.getAsJsonPrimitive("type"); 41 | if (!jsonType.isString()) { 42 | return ""; 43 | } 44 | final String type = jsonType.getAsString(); 45 | return type == null ? "" : type; 46 | } 47 | 48 | private static JsonElement extractPayload(final JsonElement json) { 49 | if (!json.isJsonObject()) { 50 | return null; 51 | } 52 | JsonObject object = json.getAsJsonObject(); 53 | if (!object.has("payload")) { 54 | return null; 55 | } 56 | return object.getAsJsonObject("payload"); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /library/src/main/java/ru/mail/im/botapi/fetcher/User.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.fetcher; 2 | 3 | public class User { 4 | private String firstName; 5 | private String lastName; 6 | private String nick; 7 | private String userId; 8 | 9 | public String getFirstName() { 10 | return firstName; 11 | } 12 | 13 | public String getNick() { 14 | return nick; 15 | } 16 | 17 | public String getUserId() { 18 | return userId; 19 | } 20 | 21 | public String getLastName() { 22 | return lastName; 23 | } 24 | } -------------------------------------------------------------------------------- /library/src/main/java/ru/mail/im/botapi/fetcher/event/CallbackQueryEvent.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.fetcher.event; 2 | 3 | import ru.mail.im.botapi.fetcher.Chat; 4 | import ru.mail.im.botapi.fetcher.User; 5 | import ru.mail.im.botapi.fetcher.event.parts.Part; 6 | 7 | import java.util.List; 8 | 9 | public class CallbackQueryEvent extends Event { 10 | 11 | @Override 12 | public OUT accept(EventVisitor visitor, IN in) { 13 | return visitor.visitCallbackQuery(this, in); 14 | } 15 | 16 | public String getCallbackData() { 17 | return withData(data -> data.callbackData); 18 | } 19 | 20 | public String getQueryId() { 21 | return withData(data -> data.queryId); 22 | } 23 | 24 | public User getFrom() { 25 | return withData(data -> data.from); 26 | } 27 | 28 | public User getMessageFrom() { 29 | return withData(data -> data.message.from); 30 | } 31 | 32 | public Chat getMessageChat() { 33 | return withData(data -> data.message.chat); 34 | } 35 | 36 | public String getMessageText() { 37 | return withData(data -> data.message.text); 38 | } 39 | 40 | public long getMessageId() { 41 | return withData(data -> data.message.msgId); 42 | } 43 | 44 | public long getMessageTimestamp() { 45 | return withData(data -> data.message.timestamp); 46 | } 47 | 48 | public List getMessageParts() { 49 | return withData(data -> data.message.parts); 50 | } 51 | 52 | static class Data { 53 | private String callbackData; 54 | private User from; 55 | private Message message; 56 | private String queryId; 57 | } 58 | 59 | static class Message { 60 | private Chat chat; 61 | private User from; 62 | private String text; 63 | private long msgId; 64 | private long timestamp; 65 | private List parts; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /library/src/main/java/ru/mail/im/botapi/fetcher/event/DeletedMessageEvent.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.fetcher.event; 2 | 3 | import ru.mail.im.botapi.fetcher.Chat; 4 | 5 | public class DeletedMessageEvent extends Event { 6 | 7 | @Override 8 | public OUT accept(EventVisitor visitor, IN in) { 9 | return visitor.visitDeletedMessage(this, in); 10 | } 11 | 12 | public long getMessageId() { 13 | return eventData.msgId; 14 | } 15 | 16 | public long getTimestamp() { 17 | return eventData.timestamp; 18 | } 19 | 20 | public Chat getChat() { 21 | return eventData.chat; 22 | } 23 | 24 | static class Data { 25 | private long msgId; 26 | private long timestamp; 27 | private Chat chat; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /library/src/main/java/ru/mail/im/botapi/fetcher/event/EditedMessageEvent.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.fetcher.event; 2 | 3 | import ru.mail.im.botapi.fetcher.Chat; 4 | import ru.mail.im.botapi.fetcher.User; 5 | 6 | public class EditedMessageEvent extends Event{ 7 | 8 | @Override 9 | public OUT accept(EventVisitor visitor, IN in) { 10 | return visitor.visitEditedMessage(this, in); 11 | } 12 | 13 | 14 | public long getMsgId() { 15 | return eventData.msgId; 16 | } 17 | 18 | public long getTimestamp() { 19 | return eventData.timestamp; 20 | } 21 | 22 | public long getEditedTimestamp() { 23 | return eventData.editedTimestamp; 24 | } 25 | 26 | public String getText() { 27 | return eventData.text; 28 | } 29 | 30 | public Chat getChat() { 31 | return eventData.chat; 32 | } 33 | 34 | public User getFrom() { 35 | return eventData.from; 36 | } 37 | 38 | static class Data { 39 | private long msgId; 40 | private long timestamp; 41 | private long editedTimestamp; 42 | private String text; 43 | private Chat chat; 44 | private User from; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /library/src/main/java/ru/mail/im/botapi/fetcher/event/Event.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.fetcher.event; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | 5 | public abstract class Event { 6 | 7 | @SerializedName("eventId") 8 | long eventId; 9 | 10 | @SerializedName("payload") 11 | T eventData; 12 | 13 | @SerializedName("type") 14 | private String type; 15 | 16 | public abstract OUT accept(EventVisitor visitor, IN in); 17 | 18 | public R withData(NonNullCall call) { 19 | if (eventData != null) { 20 | return call.call(eventData); 21 | } 22 | return null; 23 | } 24 | 25 | public interface NonNullCall { 26 | R call(D data); 27 | } 28 | 29 | public long getEventId() { 30 | return eventId; 31 | } 32 | 33 | public String getType() { 34 | return type; 35 | } 36 | } -------------------------------------------------------------------------------- /library/src/main/java/ru/mail/im/botapi/fetcher/event/EventVisitor.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.fetcher.event; 2 | 3 | public interface EventVisitor { 4 | 5 | OUT visitUnknown(UnknownEvent event, IN in); 6 | 7 | OUT visitNewMessage(NewMessageEvent event, IN in); 8 | 9 | OUT visitNewChatMembers(NewChatMembersEvent event, IN in); 10 | 11 | OUT visitLeftChatMembers(LeftChatMembersEvent leftChatMembersEvent, IN in); 12 | 13 | OUT visitDeletedMessage(DeletedMessageEvent deletedMessageEvent, IN in); 14 | 15 | OUT visitEditedMessage(EditedMessageEvent editedMessageEvent, IN in); 16 | 17 | OUT visitPinnedMessage(PinnedMessageEvent pinnedMessageEvent, IN in); 18 | 19 | OUT visitUnpinnedMessage(UnpinnedMessageEvent unpinnedMessageEvent, IN in); 20 | 21 | OUT visitCallbackQuery(CallbackQueryEvent callbackQueryEvent, IN in); 22 | } 23 | -------------------------------------------------------------------------------- /library/src/main/java/ru/mail/im/botapi/fetcher/event/LeftChatMembersEvent.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.fetcher.event; 2 | 3 | import ru.mail.im.botapi.fetcher.Chat; 4 | import ru.mail.im.botapi.fetcher.User; 5 | 6 | import java.util.List; 7 | 8 | public class LeftChatMembersEvent extends Event { 9 | @Override 10 | public OUT accept(EventVisitor visitor, IN in) { 11 | return visitor.visitLeftChatMembers(this, in); 12 | } 13 | 14 | public Chat getChat() { 15 | return eventData.chat; 16 | } 17 | 18 | public List getMembers() { 19 | return eventData.leftMembers; 20 | } 21 | 22 | public User getRemovedBy() { 23 | return eventData.removedBy; 24 | } 25 | 26 | static class Data { 27 | private Chat chat; 28 | private List leftMembers; 29 | private User removedBy; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /library/src/main/java/ru/mail/im/botapi/fetcher/event/NewChatMembersEvent.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.fetcher.event; 2 | 3 | import ru.mail.im.botapi.fetcher.Chat; 4 | import ru.mail.im.botapi.fetcher.User; 5 | 6 | import java.util.List; 7 | 8 | public class NewChatMembersEvent extends Event { 9 | 10 | @Override 11 | public OUT accept(EventVisitor visitor, IN in) { 12 | return visitor.visitNewChatMembers(this, in); 13 | } 14 | 15 | public Chat getChat() { 16 | return eventData.chat; 17 | } 18 | 19 | public List getMembers() { 20 | return eventData.newMembers; 21 | } 22 | 23 | public User getAddedBy() { 24 | return eventData.addedBy; 25 | } 26 | 27 | static class Data { 28 | private Chat chat; 29 | private List newMembers; 30 | private User addedBy; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /library/src/main/java/ru/mail/im/botapi/fetcher/event/NewMessageEvent.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.fetcher.event; 2 | 3 | import ru.mail.im.botapi.fetcher.Chat; 4 | import ru.mail.im.botapi.fetcher.User; 5 | import ru.mail.im.botapi.fetcher.event.parts.Part; 6 | 7 | import java.util.List; 8 | 9 | public class NewMessageEvent extends Event { 10 | 11 | @Override 12 | public OUT accept(EventVisitor visitor, IN in) { 13 | return visitor.visitNewMessage(this, in); 14 | } 15 | 16 | public long getMessageId() { 17 | return withData(data -> data.msgId); 18 | } 19 | 20 | public long getTimestamp() { 21 | return withData(data -> data.timestamp); 22 | } 23 | 24 | public String getText() { 25 | return withData(data -> data.text); 26 | } 27 | 28 | public User getFrom() { 29 | return withData(data -> data.from); 30 | } 31 | 32 | public Chat getChat() { 33 | return withData(data -> data.chat); 34 | } 35 | 36 | public List getParts() { 37 | return withData(data -> data.parts); 38 | } 39 | 40 | static class Data { 41 | private long msgId; 42 | private long timestamp; 43 | private String text; 44 | private Chat chat; 45 | private User from; 46 | private List parts; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /library/src/main/java/ru/mail/im/botapi/fetcher/event/PinnedMessageEvent.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.fetcher.event; 2 | 3 | import ru.mail.im.botapi.fetcher.Chat; 4 | import ru.mail.im.botapi.fetcher.User; 5 | 6 | public class PinnedMessageEvent extends Event { 7 | @Override 8 | public OUT accept(EventVisitor visitor, IN in) { 9 | return visitor.visitPinnedMessage(this, in); 10 | } 11 | 12 | public long getMsgId() { 13 | return eventData.msgId; 14 | } 15 | 16 | public long getTimestamp() { 17 | return eventData.timestamp; 18 | } 19 | 20 | public String getText() { 21 | return eventData.text; 22 | } 23 | 24 | public Chat getChat() { 25 | return eventData.chat; 26 | } 27 | 28 | public User getFrom() { 29 | return eventData.from; 30 | } 31 | 32 | static class Data { 33 | private long msgId; 34 | private long timestamp; 35 | private String text; 36 | private Chat chat; 37 | private User from; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /library/src/main/java/ru/mail/im/botapi/fetcher/event/UnknownEvent.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.fetcher.event; 2 | 3 | public class UnknownEvent extends Event { 4 | 5 | private transient String json; 6 | 7 | public UnknownEvent(final String json) { 8 | this.json = json; 9 | } 10 | 11 | public String getJson() { 12 | return json; 13 | } 14 | 15 | @Override 16 | public OUT accept(final EventVisitor visitor, final IN in) { 17 | return visitor.visitUnknown(this, in); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /library/src/main/java/ru/mail/im/botapi/fetcher/event/UnpinnedMessageEvent.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.fetcher.event; 2 | 3 | import ru.mail.im.botapi.fetcher.Chat; 4 | 5 | public class UnpinnedMessageEvent extends Event { 6 | @Override 7 | public OUT accept(EventVisitor visitor, IN in) { 8 | return visitor.visitUnpinnedMessage(this, in); 9 | } 10 | 11 | public long getMsgId() { 12 | return eventData.msgId; 13 | } 14 | 15 | public long getTimestamp() { 16 | return eventData.timestamp; 17 | } 18 | 19 | public Chat getChat() { 20 | return eventData.chat; 21 | } 22 | 23 | static class Data { 24 | private long msgId; 25 | private long timestamp; 26 | private Chat chat; 27 | } 28 | } -------------------------------------------------------------------------------- /library/src/main/java/ru/mail/im/botapi/fetcher/event/parts/File.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.fetcher.event.parts; 2 | 3 | public class File implements Part { 4 | private String fileId; 5 | 6 | public String getFileId() { 7 | return fileId; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /library/src/main/java/ru/mail/im/botapi/fetcher/event/parts/Forward.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.fetcher.event.parts; 2 | 3 | import ru.mail.im.botapi.fetcher.Message; 4 | 5 | public class Forward implements Part { 6 | private Message message; 7 | 8 | public Message getMessage() { 9 | return message; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /library/src/main/java/ru/mail/im/botapi/fetcher/event/parts/Mention.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.fetcher.event.parts; 2 | 3 | public class Mention implements Part { 4 | 5 | private String userId; 6 | private String firstName; 7 | private String lastName; 8 | 9 | public String getUserId() { 10 | return userId; 11 | } 12 | 13 | public String getFirstName() { 14 | return firstName; 15 | } 16 | 17 | public String getLastName() { 18 | return lastName; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /library/src/main/java/ru/mail/im/botapi/fetcher/event/parts/Part.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.fetcher.event.parts; 2 | 3 | public interface Part { 4 | } 5 | -------------------------------------------------------------------------------- /library/src/main/java/ru/mail/im/botapi/fetcher/event/parts/Reply.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.fetcher.event.parts; 2 | 3 | import ru.mail.im.botapi.fetcher.Message; 4 | 5 | public class Reply implements Part { 6 | private Message message; 7 | 8 | public Message getMessage() { 9 | return message; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /library/src/main/java/ru/mail/im/botapi/fetcher/event/parts/Sticker.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.fetcher.event.parts; 2 | 3 | public class Sticker implements Part { 4 | private String fileId; 5 | 6 | public String getFileId() { 7 | return fileId; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /library/src/main/java/ru/mail/im/botapi/fetcher/event/parts/Voice.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.fetcher.event.parts; 2 | 3 | public class Voice implements Part { 4 | private String fileId; 5 | 6 | public String getFileId() { 7 | return fileId; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /library/src/main/java/ru/mail/im/botapi/json/ChatTypeGsonDeserializer.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.json; 2 | 3 | import com.google.gson.JsonDeserializationContext; 4 | import com.google.gson.JsonDeserializer; 5 | import com.google.gson.JsonElement; 6 | import com.google.gson.JsonParseException; 7 | import ru.mail.im.botapi.entity.ChatType; 8 | 9 | import java.lang.reflect.Type; 10 | 11 | public class ChatTypeGsonDeserializer implements JsonDeserializer { 12 | 13 | @Override 14 | public ChatType deserialize(final JsonElement json, 15 | final Type typeOfT, 16 | final JsonDeserializationContext context) throws JsonParseException { 17 | if (json.isJsonPrimitive() && json.getAsJsonPrimitive().isString()) { 18 | String value = json.getAsString(); 19 | return ChatType.fromApiValue(value); 20 | } 21 | return null; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /library/src/main/java/ru/mail/im/botapi/response/ApiResponse.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.response; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | 5 | // TODO all fields in all GSON-parcelable classes are annotated with @SerializedName. Check that ProGuard keep that annotations. 6 | public class ApiResponse { 7 | 8 | @SerializedName("ok") 9 | private boolean ok; 10 | 11 | // optional, only if "ok" = false 12 | @SerializedName("description") 13 | private String description; 14 | 15 | public boolean isOk() { 16 | return ok; 17 | } 18 | 19 | public String getDescription() { 20 | return description; 21 | } 22 | 23 | @Override 24 | public String toString() { 25 | return "ApiResponse{" + 26 | "ok=" + ok + 27 | ", description='" + description + '\'' + 28 | '}'; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /library/src/main/java/ru/mail/im/botapi/response/ChatsGetAdminsResponse.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.response; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | import ru.mail.im.botapi.entity.Admin; 5 | 6 | import java.util.Collections; 7 | import java.util.List; 8 | 9 | public class ChatsGetAdminsResponse extends ApiResponse { 10 | 11 | @SerializedName("admins") 12 | private List admins = Collections.emptyList(); 13 | 14 | public List getAdmins() { 15 | return admins; 16 | } 17 | 18 | @Override 19 | public String toString() { 20 | return "ChatsGetAdminsResponse{" + 21 | "admins=" + admins + 22 | '}'; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /library/src/main/java/ru/mail/im/botapi/response/ChatsGetInfoResponse.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.response; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | import ru.mail.im.botapi.entity.ChatType; 5 | import ru.mail.im.botapi.entity.Photo; 6 | 7 | import java.util.Collections; 8 | import java.util.List; 9 | 10 | public class ChatsGetInfoResponse extends ApiResponse { 11 | 12 | @SerializedName("type") 13 | private ChatType type; 14 | 15 | @SerializedName("photo") 16 | private List photos = Collections.emptyList(); 17 | 18 | @SerializedName("about") 19 | private String about; 20 | 21 | /***** For private chats *****/ 22 | 23 | @SerializedName("firstName") 24 | private String firstName; 25 | 26 | @SerializedName("lastName") 27 | private String lastName; 28 | 29 | @SerializedName("nick") 30 | private String nick; 31 | 32 | @SerializedName("isBot") 33 | private boolean isBot; 34 | 35 | @SerializedName("phone") 36 | private String phone; 37 | 38 | /***** For groups and channels *****/ 39 | 40 | @SerializedName("title") 41 | private String title; 42 | 43 | @SerializedName("inviteLink") 44 | private String inviteLink; 45 | 46 | @SerializedName("public") 47 | private boolean isPublic; 48 | 49 | @SerializedName("joinModeration") 50 | private boolean hasJoinModeration; 51 | 52 | /***** For groups only *****/ 53 | @SerializedName("rules") 54 | private String rules; 55 | 56 | public ChatType getType() { 57 | return type; 58 | } 59 | 60 | public List getPhotos() { 61 | return photos; 62 | } 63 | 64 | public String getAbout() { 65 | return about; 66 | } 67 | 68 | public String getFirstName() { 69 | return firstName; 70 | } 71 | 72 | public String getLastName() { 73 | return lastName; 74 | } 75 | 76 | public String getNick() { 77 | return nick; 78 | } 79 | 80 | public boolean isBot() { 81 | return isBot; 82 | } 83 | 84 | public String getPhone() { 85 | return phone; 86 | } 87 | 88 | public String getTitle() { 89 | return title; 90 | } 91 | 92 | public String getInviteLink() { 93 | return inviteLink; 94 | } 95 | 96 | public boolean isPublic() { 97 | return isPublic; 98 | } 99 | 100 | public boolean hasJoinModeration() { 101 | return hasJoinModeration; 102 | } 103 | 104 | public String getRules() { 105 | return rules; 106 | } 107 | 108 | @Override 109 | public String toString() { 110 | return "ChatsGetInfoResponse{" + 111 | "type=" + type + 112 | ", photos=" + photos + 113 | ", about='" + about + '\'' + 114 | ", firstName='" + firstName + '\'' + 115 | ", lastName='" + lastName + '\'' + 116 | ", nick='" + nick + '\'' + 117 | ", isBot=" + isBot + 118 | ", phone='" + phone + '\'' + 119 | ", title='" + title + '\'' + 120 | ", inviteLink='" + inviteLink + '\'' + 121 | ", isPublic=" + isPublic + 122 | ", hasJoinModeration=" + hasJoinModeration + 123 | ", rules='" + rules + '\'' + 124 | '}'; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /library/src/main/java/ru/mail/im/botapi/response/MessageResponse.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.response; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | 5 | public class MessageResponse extends ApiResponse { 6 | 7 | @SerializedName("msgId") 8 | private long msgId; 9 | 10 | public long getMsgId() { 11 | return msgId; 12 | } 13 | 14 | @Override 15 | public String toString() { 16 | return "MessageResponse{" + 17 | "msgId=" + msgId + 18 | '}'; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /library/src/main/java/ru/mail/im/botapi/response/SelfGetResponse.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.response; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | 5 | import java.util.Collections; 6 | import java.util.List; 7 | 8 | public class SelfGetResponse extends ApiResponse { 9 | 10 | @SerializedName("userId") 11 | private String userId; 12 | 13 | @SerializedName("firstName") 14 | private String firstName; 15 | 16 | @SerializedName("lastName") 17 | private String lastName; 18 | 19 | @SerializedName("nick") 20 | private String nick; 21 | 22 | @SerializedName("about") 23 | private String about; 24 | 25 | @SerializedName("photo") 26 | private List photo = Collections.emptyList(); 27 | 28 | public String getUserId() { 29 | return userId; 30 | } 31 | 32 | public String getFirstName() { 33 | return firstName; 34 | } 35 | 36 | public String getLastName() { 37 | return lastName; 38 | } 39 | 40 | public String getNick() { 41 | return nick; 42 | } 43 | 44 | public String getAbout() { 45 | return about; 46 | } 47 | 48 | public List getPhoto() { 49 | return photo; 50 | } 51 | 52 | @Override 53 | public String toString() { 54 | return "SelfGetResponse{" + 55 | "userId='" + userId + '\'' + 56 | ", firstName='" + firstName + '\'' + 57 | ", lastName='" + lastName + '\'' + 58 | ", nick='" + nick + '\'' + 59 | ", about='" + about + '\'' + 60 | ", photo=" + photo + 61 | '}'; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /library/src/main/java/ru/mail/im/botapi/util/IOBackoff.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.util; 2 | 3 | import java.io.IOException; 4 | 5 | public class IOBackoff { 6 | 7 | private final long startTime; 8 | private final long maxTime; 9 | private final double factor; 10 | private final Sleeping sleeping; 11 | 12 | private long currentTime; 13 | 14 | private IOBackoff(final Builder builder) { 15 | startTime = builder.startTime; 16 | maxTime = builder.maxTime; 17 | factor = builder.factor; 18 | sleeping = builder.sleeping; 19 | 20 | reset(); 21 | } 22 | 23 | public void execute(final IOOperation operation) { 24 | try { 25 | operation.execute(); 26 | reset(); 27 | } catch (Exception ignored) { 28 | waitNext(); 29 | } 30 | } 31 | 32 | private void reset() { 33 | currentTime = startTime; 34 | } 35 | 36 | private void waitNext() { 37 | try { 38 | sleeping.sleep(currentTime); 39 | currentTime = Math.min(maxTime, (long) (currentTime * factor)); 40 | } catch (InterruptedException e) { 41 | Thread.currentThread().interrupt(); 42 | } 43 | } 44 | 45 | public static Builder newBuilder() { 46 | return new Builder(Thread::sleep); 47 | } 48 | 49 | static Builder newTestBuilder(final Sleeping sleeping) { 50 | return new Builder(sleeping); 51 | } 52 | 53 | public static final class Builder { 54 | 55 | private final Sleeping sleeping; 56 | 57 | private long startTime; 58 | private long maxTime; 59 | private double factor; 60 | 61 | private Builder(final Sleeping sleeping) { 62 | this.sleeping = sleeping; 63 | } 64 | 65 | public Builder startTime(final long startTime) { 66 | this.startTime = startTime; 67 | return this; 68 | } 69 | 70 | public Builder maxTime(final long maxTime) { 71 | this.maxTime = maxTime; 72 | return this; 73 | } 74 | 75 | public Builder factor(final double factor) { 76 | this.factor = factor; 77 | return this; 78 | } 79 | 80 | public IOBackoff build() { 81 | if (startTime <= 0) { 82 | throw new IllegalArgumentException("startTime must be greater than zero"); 83 | } 84 | if (maxTime <= 0) { 85 | throw new IllegalArgumentException("maxTime must be greater than zero"); 86 | } 87 | if (factor <= 0) { 88 | throw new IllegalArgumentException("factor must be greater than zero"); 89 | } 90 | if (startTime > maxTime) { 91 | throw new IllegalArgumentException("startTime must be less or equal to maxTime"); 92 | } 93 | return new IOBackoff(this); 94 | } 95 | } 96 | 97 | public interface IOOperation { 98 | void execute() throws IOException; 99 | } 100 | 101 | interface Sleeping { 102 | void sleep(final long ms) throws InterruptedException; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /library/src/main/java/ru/mail/im/botapi/util/ListenerDescriptor.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.util; 2 | 3 | public interface ListenerDescriptor { 4 | void remove(); 5 | } 6 | -------------------------------------------------------------------------------- /library/src/main/java/ru/mail/im/botapi/util/ListenerList.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.util; 2 | 3 | import javax.annotation.concurrent.GuardedBy; 4 | import java.lang.reflect.Proxy; 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | public class ListenerList { 9 | 10 | @GuardedBy("this") 11 | private final List descriptors = new ArrayList<>(); 12 | 13 | private final T proxy; 14 | 15 | public ListenerList(final Class clazz) { 16 | //noinspection unchecked 17 | proxy = (T) Proxy.newProxyInstance(clazz.getClassLoader(), 18 | new Class[]{clazz}, 19 | (proxy, method, args) -> { 20 | synchronized (ListenerList.class) { 21 | for (Descriptor descriptor : descriptors) { 22 | method.invoke(descriptor.listener, args); 23 | } 24 | } 25 | return null; 26 | }); 27 | } 28 | 29 | public synchronized ListenerDescriptor add(final T listener) { 30 | final Descriptor descriptor = new Descriptor(listener); 31 | descriptors.add(descriptor); 32 | return descriptor; 33 | } 34 | 35 | public T asListener() { 36 | return proxy; 37 | } 38 | 39 | private class Descriptor implements ListenerDescriptor { 40 | 41 | private final T listener; 42 | 43 | Descriptor(final T listener) { 44 | this.listener = listener; 45 | } 46 | 47 | @Override 48 | public void remove() { 49 | synchronized (ListenerList.class) { 50 | descriptors.remove(this); 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /library/src/test/java/ru/mail/im/botapi/api/ApiImplementationFactoryTest.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.api; 2 | 3 | import com.google.gson.Gson; 4 | import okhttp3.MultipartBody; 5 | import okhttp3.Request; 6 | import okhttp3.RequestBody; 7 | import okio.Buffer; 8 | import org.junit.Rule; 9 | import org.junit.Test; 10 | import org.junit.rules.ExpectedException; 11 | import org.junit.rules.TemporaryFolder; 12 | 13 | import javax.annotation.Nullable; 14 | import java.io.File; 15 | import java.io.IOException; 16 | import java.nio.file.Files; 17 | 18 | import static org.hamcrest.CoreMatchers.is; 19 | import static org.junit.Assert.assertEquals; 20 | import static org.junit.Assert.assertNotNull; 21 | import static org.junit.Assert.assertNull; 22 | import static org.junit.Assert.assertTrue; 23 | 24 | public class ApiImplementationFactoryTest { 25 | 26 | private Request executedRequest; 27 | 28 | private final RequestExecutor requestExecutor = new RequestExecutor() { 29 | @Override 30 | public T execute(final Request request, final Class responseClass) { 31 | executedRequest = request; 32 | return null; 33 | } 34 | }; 35 | 36 | private final ApiImplementationFactory factory = new ApiImplementationFactory( 37 | new Gson(), 38 | requestExecutor, 39 | "https://u.icq.net/rapi/botapi", 40 | "qwert" 41 | ); 42 | 43 | @Rule 44 | public ExpectedException expectedException = ExpectedException.none(); 45 | 46 | @Rule 47 | public TemporaryFolder temporaryFolder = new TemporaryFolder(); 48 | 49 | @Test 50 | public void getNoParams() throws Exception { 51 | final TestApi api = factory.createImplementation(TestApi.class); 52 | api.getNoParams(); 53 | 54 | assertNotNull(executedRequest); 55 | assertEquals("https://u.icq.net/rapi/botapi/get/no-params?token=qwert", executedRequest.url().toString()); 56 | assertNull(executedRequest.body()); 57 | } 58 | 59 | @Test 60 | public void postNoParams() throws Exception { 61 | final TestApi api = factory.createImplementation(TestApi.class); 62 | api.postNoParams(); 63 | 64 | assertNotNull(executedRequest); 65 | assertEquals("https://u.icq.net/rapi/botapi/post/no-params", executedRequest.url().toString()); 66 | assertMultipartBody(executedRequest, "form-data; name=\"token\"", "qwert"); 67 | } 68 | 69 | @Test 70 | public void getWithParams() throws Exception { 71 | final TestApi api = factory.createImplementation(TestApi.class); 72 | api.getUser(123L); 73 | 74 | assertNotNull(executedRequest); 75 | assertEquals("https://u.icq.net/rapi/botapi/get/user?token=qwert&user_id=123", executedRequest.url().toString()); 76 | assertNull(executedRequest.body()); 77 | } 78 | 79 | @Test 80 | public void postWithParams() throws Exception { 81 | final TestApi api = factory.createImplementation(TestApi.class); 82 | api.postUser(123L); 83 | 84 | assertNotNull(executedRequest); 85 | assertEquals("https://u.icq.net/rapi/botapi/post/user", executedRequest.url().toString()); 86 | assertMultipartBody(executedRequest, 87 | "form-data; name=\"token\"", "qwert", 88 | "form-data; name=\"user_id\"", "123"); 89 | } 90 | 91 | @Test 92 | public void postFile() throws Exception { 93 | final File file = temporaryFolder.newFile("qaz.txt"); 94 | Files.write(file.toPath(), "Hello".getBytes()); 95 | 96 | factory.createImplementation(TestApi.class).postFile(file); 97 | 98 | assertNotNull(executedRequest); 99 | assertEquals("https://u.icq.net/rapi/botapi/post/file", executedRequest.url().toString()); 100 | assertMultipartBody(executedRequest, 101 | "form-data; name=\"token\"", "qwert", 102 | "form-data; name=\"some_file\"; filename=\"qaz.txt\"", "Hello"); 103 | } 104 | 105 | @Test 106 | public void notAnnotatedMethod() throws Exception { 107 | expectedException.expect(IllegalArgumentException.class); 108 | expectedException.expectMessage(is("Request must be GET or POST")); 109 | factory.createImplementation(TestApi.class).notAnnotated(); 110 | } 111 | 112 | @Test 113 | public void notThrowingIOException() { 114 | expectedException.expect(RuntimeException.class); 115 | expectedException.expectMessage(is("Request method must throw IOException")); 116 | factory.createImplementation(TestApi.class).noException(); 117 | } 118 | 119 | private static void assertMultipartBody(final Request request, final String... expectedParts) throws IOException { 120 | assertNotNull(request.body()); 121 | 122 | assertTrue(request.body() instanceof MultipartBody); 123 | final MultipartBody body = (MultipartBody) request.body(); 124 | 125 | assertNotNull(body.contentType()); 126 | assertEquals("multipart", body.contentType().type()); 127 | assertEquals("form-data", body.contentType().subtype()); 128 | 129 | assertEquals(expectedParts.length / 2, body.parts().size()); 130 | for (int i = 0; i < body.parts().size(); i++) { 131 | final MultipartBody.Part actualPart = body.part(i); 132 | assertNotNull(actualPart.headers()); 133 | assertEquals(expectedParts[2 * i], actualPart.headers().get("Content-Disposition")); 134 | assertEquals(expectedParts[2 * i + 1], bodyToString(actualPart.body())); 135 | } 136 | } 137 | 138 | @Nullable 139 | private static String bodyToString(@Nullable final RequestBody body) throws IOException { 140 | if (body == null) { 141 | return null; 142 | } 143 | final Buffer buffer = new Buffer(); 144 | body.writeTo(buffer); 145 | return buffer.readUtf8(); 146 | } 147 | 148 | interface TestApi { 149 | 150 | @GetRequest("get/no-params") 151 | void getNoParams() throws IOException; 152 | 153 | @PostRequest("post/no-params") 154 | void postNoParams() throws IOException; 155 | 156 | @GetRequest("get/user") 157 | void getUser(@RequestParam("user_id") long id) throws IOException; 158 | 159 | @PostRequest("post/user") 160 | void postUser(@RequestParam("user_id") long id) throws IOException; 161 | 162 | @PostRequest("post/file") 163 | void postFile(@RequestParam("some_file") File file) throws IOException; 164 | 165 | void notAnnotated() throws IOException; 166 | 167 | void noException(); 168 | } 169 | } -------------------------------------------------------------------------------- /library/src/test/java/ru/mail/im/botapi/api/QueryBuilderTest.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.api; 2 | 3 | import com.google.gson.Gson; 4 | import org.junit.Test; 5 | 6 | import java.lang.reflect.Method; 7 | import java.util.List; 8 | 9 | import static org.junit.Assert.assertEquals; 10 | import static org.junit.Assert.assertTrue; 11 | import static org.junit.Assert.fail; 12 | 13 | public class QueryBuilderTest { 14 | 15 | @Test 16 | public void noArg() throws Exception { 17 | final Method method = Foo.class.getDeclaredMethod("noArg"); 18 | List params = new QueryBuilder(new Gson(), method, null).build(); 19 | assertTrue(params.isEmpty()); 20 | } 21 | 22 | @Test 23 | public void noArg2() throws Exception { 24 | final Method method = Foo.class.getDeclaredMethod("noArg"); 25 | List params = new QueryBuilder(new Gson(), method, new Object[0]).build(); 26 | assertTrue(params.isEmpty()); 27 | } 28 | 29 | @Test 30 | public void oneSingleArg() throws Exception { 31 | final Method method = Foo.class.getDeclaredMethod("oneArg", String.class); 32 | List params = new QueryBuilder(new Gson(), method, new Object[]{"Hello"}).build(); 33 | assertEquals(1, params.size()); 34 | assertEquals("test", params.get(0).name); 35 | assertEquals("Hello", params.get(0).value); 36 | } 37 | 38 | @Test 39 | public void oneArrayArg() throws Exception { 40 | final Method method = Foo.class.getDeclaredMethod("oneArrayArg", int[].class); 41 | List params = new QueryBuilder(new Gson(), method, new Object[]{new int[]{111, 222}}).build(); 42 | assertEquals(2, params.size()); 43 | 44 | assertEquals("values", params.get(0).name); 45 | assertEquals(111, params.get(0).value); 46 | 47 | assertEquals("values", params.get(1).name); 48 | assertEquals(222, params.get(1).value); 49 | } 50 | 51 | @Test 52 | public void noArgAnnotation() throws Exception { 53 | final Method method = Foo.class.getDeclaredMethod("noAnnotation", int.class, boolean.class, String.class); 54 | try { 55 | new QueryBuilder(new Gson(), method, new Object[]{123, true, "qwe"}).build(); 56 | fail(); 57 | } catch (IllegalArgumentException e) { 58 | assertEquals("Parameter with index 1 was not annotated with @RequestParam", e.getMessage()); 59 | } 60 | } 61 | 62 | interface Foo { 63 | void noArg(); 64 | 65 | void oneArg(@RequestParam("test") String s); 66 | 67 | void oneArrayArg(@RequestParam("values") int[] arr); 68 | 69 | void noAnnotation(@RequestParam("a") int a, boolean b, @RequestParam("c") String c); 70 | } 71 | } -------------------------------------------------------------------------------- /library/src/test/java/ru/mail/im/botapi/api/QueryParameterTest.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.api; 2 | 3 | import com.google.gson.Gson; 4 | import org.junit.Test; 5 | 6 | import java.util.Collections; 7 | 8 | import static org.junit.Assert.assertEquals; 9 | import static org.junit.Assert.assertNull; 10 | 11 | public class QueryParameterTest { 12 | 13 | @Test 14 | public void getValueAsStringNull() { 15 | assertNull(new QueryParameter(new Gson(), "qwe", null).getValueAsString()); 16 | } 17 | 18 | @Test 19 | public void getValueAsStringInt() { 20 | assertEquals("123", new QueryParameter(new Gson(), "qwe", 123).getValueAsString()); 21 | } 22 | 23 | @Test 24 | public void getValueAsStringBool() { 25 | assertEquals("true", new QueryParameter(new Gson(), "qwe", true).getValueAsString()); 26 | } 27 | 28 | @Test 29 | public void getValueAsStringString() { 30 | assertEquals("value", new QueryParameter(new Gson(), "qwe", "value").getValueAsString()); 31 | } 32 | 33 | @Test 34 | public void getValueAsStringPojo() { 35 | assertEquals("[\"value\"]", new QueryParameter(new Gson(), "qwe", Collections.singletonList("value")).getValueAsString()); 36 | } 37 | } -------------------------------------------------------------------------------- /library/src/test/java/ru/mail/im/botapi/util/IOBackoffTest.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.util; 2 | 3 | import org.junit.Rule; 4 | import org.junit.Test; 5 | import org.junit.rules.ExpectedException; 6 | 7 | import java.io.IOException; 8 | import java.util.ArrayList; 9 | import java.util.Arrays; 10 | import java.util.Collections; 11 | import java.util.List; 12 | 13 | import static org.junit.Assert.assertEquals; 14 | import static org.junit.Assert.assertTrue; 15 | 16 | public class IOBackoffTest { 17 | 18 | private final FakeSleeping fakeSleeping = new FakeSleeping(); 19 | 20 | @Rule 21 | public ExpectedException thrown = ExpectedException.none(); 22 | 23 | @Test 24 | public void executeSingleFail() { 25 | final IOBackoff backoff = IOBackoff.newTestBuilder(fakeSleeping) 26 | .startTime(10) 27 | .maxTime(1000) 28 | .factor(1.5) 29 | .build(); 30 | 31 | backoff.execute(new FailOperation()); 32 | 33 | assertEquals(Collections.singletonList(10L), fakeSleeping.sleepTimeList); 34 | } 35 | 36 | @Test 37 | public void executeTwoSequentialFails() { 38 | final IOBackoff backoff = IOBackoff.newTestBuilder(fakeSleeping) 39 | .startTime(10) 40 | .maxTime(1000) 41 | .factor(50) 42 | .build(); 43 | 44 | backoff.execute(new FailOperation()); 45 | backoff.execute(new FailOperation()); 46 | 47 | assertEquals(Arrays.asList(10L, 500L), fakeSleeping.sleepTimeList); 48 | } 49 | 50 | @Test 51 | public void executeTwoNonSequentialFails() { 52 | final IOBackoff backoff = IOBackoff.newTestBuilder(fakeSleeping) 53 | .startTime(10) 54 | .maxTime(1000) 55 | .factor(50) 56 | .build(); 57 | 58 | backoff.execute(new FailOperation()); 59 | backoff.execute(new SuccessOperation()); 60 | backoff.execute(new FailOperation()); 61 | 62 | assertEquals(Arrays.asList(10L, 10L), fakeSleeping.sleepTimeList); 63 | } 64 | 65 | @Test 66 | public void executeWithMaxLimit() { 67 | final IOBackoff backoff = IOBackoff.newTestBuilder(fakeSleeping) 68 | .startTime(10) 69 | .maxTime(50) 70 | .factor(2) 71 | .build(); 72 | 73 | for (int i = 0; i < 5; i++) { 74 | backoff.execute(new FailOperation()); 75 | } 76 | 77 | assertEquals(Arrays.asList(10L, 20L, 40L, 50L, 50L), fakeSleeping.sleepTimeList); 78 | } 79 | 80 | @Test 81 | public void illegalStartTime() { 82 | thrown.expect(IllegalArgumentException.class); 83 | thrown.expectMessage("startTime must be greater than zero"); 84 | IOBackoff.newBuilder() 85 | .startTime(-1) 86 | .maxTime(1000) 87 | .factor(2) 88 | .build(); 89 | } 90 | 91 | @Test 92 | public void illegalMaxTime() { 93 | thrown.expect(IllegalArgumentException.class); 94 | thrown.expectMessage("maxTime must be greater than zero"); 95 | IOBackoff.newBuilder() 96 | .startTime(10) 97 | .maxTime(-1) 98 | .factor(2) 99 | .build(); 100 | } 101 | 102 | @Test 103 | public void illegalFactor() { 104 | thrown.expect(IllegalArgumentException.class); 105 | thrown.expectMessage("factor must be greater than zero"); 106 | IOBackoff.newBuilder() 107 | .startTime(10) 108 | .maxTime(20) 109 | .factor(0) 110 | .build(); 111 | } 112 | 113 | @Test 114 | public void illegalStartAndMaxTime() { 115 | thrown.expect(IllegalArgumentException.class); 116 | thrown.expectMessage("startTime must be less or equal to maxTime"); 117 | IOBackoff.newBuilder() 118 | .startTime(20) 119 | .maxTime(10) 120 | .factor(2) 121 | .build(); 122 | } 123 | 124 | @Test 125 | public void failSleep() { 126 | final IOBackoff.Sleeping failSleeping = ms -> { 127 | throw new InterruptedException("Fake"); 128 | }; 129 | IOBackoff.newTestBuilder(failSleeping) 130 | .startTime(10) 131 | .maxTime(20) 132 | .factor(2) 133 | .build() 134 | .execute(new FailOperation()); 135 | 136 | assertTrue(Thread.interrupted()); 137 | } 138 | 139 | private static class FailOperation implements IOBackoff.IOOperation { 140 | 141 | @Override 142 | public void execute() throws IOException { 143 | throw new IOException("Fake"); 144 | } 145 | } 146 | 147 | private static class SuccessOperation implements IOBackoff.IOOperation { 148 | 149 | @Override 150 | public void execute() { 151 | } 152 | } 153 | 154 | private static class FakeSleeping implements IOBackoff.Sleeping { 155 | 156 | final List sleepTimeList = new ArrayList<>(); 157 | 158 | @Override 159 | public void sleep(final long ms) { 160 | sleepTimeList.add(ms); 161 | } 162 | } 163 | } -------------------------------------------------------------------------------- /library/src/test/java/ru/mail/im/botapi/util/ListenerListTest.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.util; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.assertEquals; 6 | import static org.junit.Assert.assertNull; 7 | 8 | public class ListenerListTest { 9 | 10 | private final ListenerList list = new ListenerList<>(Foo.class); 11 | 12 | @Test 13 | public void addNotify() { 14 | final FooImpl foo = new FooImpl(); 15 | list.add(foo); 16 | 17 | list.asListener().bar("Hello"); 18 | 19 | assertEquals("Hello", foo.lastValue); 20 | } 21 | 22 | @Test 23 | public void addRemove() { 24 | final FooImpl foo = new FooImpl(); 25 | list.add(foo).remove(); 26 | 27 | list.asListener().bar("Hello"); 28 | 29 | assertNull(foo.lastValue); 30 | } 31 | 32 | @Test 33 | public void twoListeners() { 34 | final FooImpl foo1 = new FooImpl(); 35 | final FooImpl foo2 = new FooImpl(); 36 | 37 | final ListenerDescriptor descriptor = list.add(foo1); 38 | list.add(foo2); 39 | descriptor.remove(); 40 | 41 | list.asListener().bar("World"); 42 | 43 | assertNull(foo1.lastValue); 44 | assertEquals("World", foo2.lastValue); 45 | } 46 | 47 | interface Foo { 48 | void bar(String value); 49 | } 50 | 51 | static class FooImpl implements Foo { 52 | 53 | String lastValue; 54 | 55 | @Override 56 | public void bar(final String value) { 57 | lastValue = value; 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /sample/.gitignore: -------------------------------------------------------------------------------- 1 | /out 2 | /build -------------------------------------------------------------------------------- /sample/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | } 4 | 5 | group 'ru.mail.im' 6 | version '1.0.0' 7 | 8 | sourceCompatibility = versions.javaVersion 9 | 10 | repositories { 11 | mavenCentral() 12 | } 13 | 14 | dependencies { 15 | compile project(':library') 16 | compile group: 'org.slf4j', name: 'slf4j-simple', version: '1.7.28' 17 | 18 | testCompile "junit:junit:${versions.junit}" 19 | } -------------------------------------------------------------------------------- /sample/src/main/java/ru/mail/im/botapi/sample/AppCommandHandler.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.sample; 2 | 3 | import ru.mail.im.botapi.BotApiClient; 4 | import ru.mail.im.botapi.entity.ChatAction; 5 | import ru.mail.im.botapi.response.ApiResponse; 6 | import ru.mail.im.botapi.sample.command.CommandHandler; 7 | import ru.mail.im.botapi.util.ListenerDescriptor; 8 | 9 | import javax.annotation.Nullable; 10 | import java.io.File; 11 | import java.io.FileNotFoundException; 12 | import java.io.IOException; 13 | import java.util.ArrayList; 14 | import java.util.Arrays; 15 | import java.util.Collections; 16 | import java.util.List; 17 | import java.util.stream.Collectors; 18 | 19 | class AppCommandHandler implements CommandHandler { 20 | 21 | @Nullable 22 | private BotApiClient client; 23 | 24 | @Nullable 25 | private ListenerDescriptor fetchListenerDescriptor; 26 | 27 | @Nullable 28 | private FileFetchWriter fetchWriter; 29 | 30 | private final List requestExecuteListeners = new ArrayList<>(); 31 | 32 | void addOnRequestExecuteListener(final OnRequestExecuteListener listener) { 33 | requestExecuteListeners.add(listener); 34 | } 35 | 36 | @Override 37 | public void onStart(final String token) { 38 | client = new BotApiClient(token, 0, 1500); 39 | client.start(); 40 | } 41 | 42 | @Override 43 | public void onExit() { 44 | if (client != null) { 45 | client.stop(); 46 | client = null; 47 | } 48 | if (fetchListenerDescriptor != null) { 49 | fetchListenerDescriptor.remove(); 50 | } 51 | if (fetchWriter != null) { 52 | fetchWriter.close(); 53 | } 54 | } 55 | 56 | @Override 57 | public void onSelf() { 58 | api(client -> client.self().get()); 59 | } 60 | 61 | @Override 62 | public void onSendText(final String chatId, final String text) { 63 | api(client -> client.messages().sendText(chatId, text, null, null, null, null, null, null)); 64 | } 65 | 66 | @Override 67 | public void onSendFile(final String chatId, final File file) { 68 | api(client -> client.messages().sendFile(chatId, file, null, null, null, null, null)); 69 | } 70 | 71 | @Override 72 | public void onSendFile(final String chatId, final File file, final String caption) { 73 | api(client -> client.messages().sendFile(chatId, file, caption, null, null, null, null)); 74 | } 75 | 76 | @Override 77 | public void onSendVoice(final String chatId, final File file) { 78 | System.out.format("Send voice '%s' to chat %s%n", file, chatId); 79 | } 80 | 81 | @Override 82 | public void onEditText(final String chatId, final long msgId, final String newText) { 83 | api(client -> client.messages().editText(chatId, msgId, newText, null)); 84 | } 85 | 86 | @Override 87 | public void onDelete(final String chatId, final long msgId) { 88 | api(client -> client.messages().deleteMessages(chatId, new long[]{msgId})); 89 | } 90 | 91 | @Override 92 | public void onDelete(final String chatId, final long[] msgIds) { 93 | api(client -> client.messages().deleteMessages(chatId, msgIds)); 94 | } 95 | 96 | @Override 97 | public void onSleep(final long ms) { 98 | try { 99 | System.out.println("Sleep for " + ms + " ms"); 100 | Thread.sleep(ms); 101 | } catch (InterruptedException e) { 102 | System.err.println(e.getMessage()); 103 | } 104 | } 105 | 106 | @Override 107 | public void onChatAction(final String chatId, final ChatAction... actions) { 108 | api(client -> client.chats().sendActions(chatId, actions)); 109 | } 110 | 111 | @Override 112 | public void onGetChatInfo(final String chatId) { 113 | api(client -> client.chats().getInfo(chatId)); 114 | } 115 | 116 | @Override 117 | public void onGetChatAdmins(final String chatId) { 118 | api(client -> client.chats().getAdmins(chatId)); 119 | } 120 | 121 | @Override 122 | public void onFetch(final File toFile) { 123 | try { 124 | fetchWriter = new FileFetchWriter(toFile); 125 | internal(client -> fetchListenerDescriptor = client.addOnEventFetchListener(events -> fetchWriter.write(events))); 126 | } catch (FileNotFoundException e) { 127 | System.err.println(e.getMessage()); 128 | } 129 | } 130 | 131 | private void api(ApiOperation operation) { 132 | if (client == null) { 133 | throw new IllegalStateException("Client not started"); 134 | } 135 | try { 136 | invokeListeners(operation.execute(client)); 137 | } catch (IOException e) { 138 | System.err.println("Fail to execute request: " + e.getMessage()); 139 | } 140 | } 141 | 142 | private void internal(InternalOperation operation) { 143 | if (client == null) { 144 | throw new IllegalStateException("Client not started"); 145 | } 146 | operation.execute(client); 147 | } 148 | 149 | private void invokeListeners(final ApiResponse response) { 150 | for (OnRequestExecuteListener listener : requestExecuteListeners) { 151 | listener.onRequestExecute(response); 152 | } 153 | } 154 | 155 | @FunctionalInterface 156 | private interface ApiOperation { 157 | T execute(BotApiClient client) throws IOException; 158 | } 159 | 160 | @FunctionalInterface 161 | private interface InternalOperation { 162 | void execute(BotApiClient client); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /sample/src/main/java/ru/mail/im/botapi/sample/FileFetchWriter.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.sample; 2 | 3 | import ru.mail.im.botapi.fetcher.event.*; 4 | 5 | import java.io.Closeable; 6 | import java.io.File; 7 | import java.io.FileNotFoundException; 8 | import java.io.PrintStream; 9 | import java.util.List; 10 | 11 | class FileFetchWriter implements Closeable { 12 | 13 | private final PrintStream stream; 14 | 15 | private final EventVisitor visitor = new EventVisitor() { 16 | 17 | @Override 18 | public Void visitUnknown(final UnknownEvent event, final PrintStream stream) { 19 | stream.println("# UNKNOWN EVENT"); 20 | stream.format("JSON: %s%n", event.getJson()); 21 | return null; 22 | } 23 | 24 | @Override 25 | public Void visitNewMessage(NewMessageEvent event, PrintStream printStream) { 26 | stream.format("Event type :%s%n", event.getType()); 27 | stream.format("Chat :%s%n", event.getChat().getChatId()); 28 | stream.format("From :%s%n", event.getFrom().getNick()); 29 | stream.format("Text :%s%n", event.getText()); 30 | stream.format("Time :%s%n", event.getTimestamp()); 31 | return null; 32 | } 33 | 34 | @Override 35 | public Void visitNewChatMembers(NewChatMembersEvent event, PrintStream printStream) { 36 | stream.format("Event type :%s%n", event.getType()); 37 | stream.format("Chat :%s%n", event.getChat().getChatId()); 38 | stream.format("From :%s%n", event.getAddedBy().getNick()); 39 | stream.format("New members :%s%n", event.getMembers()); 40 | return null; 41 | } 42 | 43 | @Override 44 | public Void visitLeftChatMembers(LeftChatMembersEvent event, PrintStream printStream) { 45 | stream.format("Event type :%s%n", event.getType()); 46 | stream.format("Chat :%s%n", event.getChat().getChatId()); 47 | stream.format("From :%s%n", event.getRemovedBy().getNick()); 48 | stream.format("Left members :%s%n", event.getMembers()); 49 | return null; 50 | } 51 | 52 | @Override 53 | public Void visitDeletedMessage(DeletedMessageEvent event, PrintStream printStream) { 54 | stream.format("Event type :%s%n", event.getType()); 55 | stream.format("Chat :%s%n", event.getChat().getChatId()); 56 | stream.format("MsgId :%s%n", event.getMessageId()); 57 | stream.format("Timestamp :%s%n", event.getTimestamp()); 58 | return null; 59 | } 60 | 61 | @Override 62 | public Void visitEditedMessage(EditedMessageEvent event, PrintStream printStream) { 63 | stream.format("Event type :%s%n", event.getType()); 64 | stream.format("Chat :%s%n", event.getChat().getChatId()); 65 | stream.format("Text :%s%n", event.getText()); 66 | stream.format("Timestamp :%s%n", event.getTimestamp()); 67 | stream.format("Edited timestamp :%s%n", event.getEditedTimestamp()); 68 | stream.format("User :%s%n", event.getFrom().getNick()); 69 | return null; 70 | } 71 | 72 | @Override 73 | public Void visitPinnedMessage(PinnedMessageEvent event, PrintStream printStream) { 74 | stream.format("Event type :%s%n", event.getType()); 75 | stream.format("Chat :%s%n", event.getChat().getChatId()); 76 | stream.format("Text :%s%n", event.getText()); 77 | stream.format("Timestamp :%s%n", event.getTimestamp()); 78 | stream.format("User :%s%n", event.getFrom().getNick()); 79 | return null; 80 | } 81 | 82 | @Override 83 | public Void visitUnpinnedMessage(UnpinnedMessageEvent event, PrintStream printStream) { 84 | stream.format("Event type :%s%n", event.getType()); 85 | stream.format("Chat :%s%n", event.getChat().getChatId()); 86 | stream.format("Timestamp :%s%n", event.getTimestamp()); 87 | return null; 88 | } 89 | 90 | @Override 91 | public Void visitCallbackQuery(final CallbackQueryEvent event, final PrintStream printStream) { 92 | stream.format("Event type :%s%n", event.getType()); 93 | stream.format("Message chat :%s%n", event.getMessageChat().getChatId()); 94 | stream.format("Message timestamp :%s%n", event.getMessageTimestamp()); 95 | stream.format("CallbackData :%s%n", event.getCallbackData()); 96 | stream.format("Callback query id :%s%n", event.getQueryId()); 97 | return null; 98 | } 99 | }; 100 | 101 | FileFetchWriter(final File file) throws FileNotFoundException { 102 | stream = new PrintStream(file); 103 | } 104 | 105 | void write(final List events) { 106 | for (Event event : events) { 107 | event.accept(visitor, stream); 108 | stream.println(); 109 | } 110 | } 111 | 112 | @Override 113 | public void close() { 114 | stream.close(); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /sample/src/main/java/ru/mail/im/botapi/sample/Main.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.sample; 2 | 3 | import ru.mail.im.botapi.sample.command.CommandProcessor; 4 | 5 | import java.io.FileInputStream; 6 | import java.io.IOException; 7 | import java.io.InputStream; 8 | 9 | public class Main { 10 | public static void main(final String[] args) throws IOException { 11 | InputStream in = System.in; 12 | if (args.length == 1) { 13 | in = new FileInputStream(args[0]); 14 | } 15 | 16 | final PlaceholderValueProviderImpl placeholderValueProvider = new PlaceholderValueProviderImpl(); 17 | 18 | final AppCommandHandler appCommandHandler = new AppCommandHandler(); 19 | appCommandHandler.addOnRequestExecuteListener(placeholderValueProvider); 20 | appCommandHandler.addOnRequestExecuteListener(new ResponsePrinter()); 21 | 22 | final CommandProcessor parser = new CommandProcessor(in, System.out, appCommandHandler); 23 | parser.setPlaceholderValueProvider(placeholderValueProvider); 24 | parser.start(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /sample/src/main/java/ru/mail/im/botapi/sample/OnRequestExecuteListener.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.sample; 2 | 3 | import ru.mail.im.botapi.response.ApiResponse; 4 | 5 | interface OnRequestExecuteListener { 6 | void onRequestExecute(final ApiResponse response); 7 | } 8 | -------------------------------------------------------------------------------- /sample/src/main/java/ru/mail/im/botapi/sample/PlaceholderValueProviderImpl.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.sample; 2 | 3 | import ru.mail.im.botapi.response.ApiResponse; 4 | import ru.mail.im.botapi.response.MessageResponse; 5 | import ru.mail.im.botapi.sample.command.PlaceholderValueProvider; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | import java.util.regex.Matcher; 10 | import java.util.regex.Pattern; 11 | 12 | class PlaceholderValueProviderImpl implements OnRequestExecuteListener, PlaceholderValueProvider { 13 | 14 | private final Pattern msgIdPattern = Pattern.compile("msgId\\[(-?[0-9]+)\\]"); 15 | 16 | private final List lastMessageIds = new ArrayList<>(); 17 | 18 | @Override 19 | public void onRequestExecute(final ApiResponse response) { 20 | if (response instanceof MessageResponse) { 21 | lastMessageIds.add(((MessageResponse) response).getMsgId()); 22 | } 23 | } 24 | 25 | @Override 26 | public String provide(final String name) { 27 | Matcher matcher = msgIdPattern.matcher(name); 28 | if (matcher.find()) { 29 | int index = Integer.parseInt(matcher.group(1)); 30 | if (index < 0) { 31 | index += lastMessageIds.size(); 32 | } 33 | return lastMessageIds.get(index).toString(); 34 | } 35 | return null; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /sample/src/main/java/ru/mail/im/botapi/sample/ResponsePrinter.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.sample; 2 | 3 | import com.google.gson.*; 4 | import ru.mail.im.botapi.entity.ChatType; 5 | import ru.mail.im.botapi.response.ApiResponse; 6 | 7 | import java.lang.reflect.Type; 8 | 9 | public class ResponsePrinter implements OnRequestExecuteListener { 10 | 11 | private final Gson gson = new GsonBuilder() 12 | .disableHtmlEscaping() 13 | .registerTypeAdapter(ChatType.class, new ChatTypeSerializer()) 14 | .create(); 15 | 16 | @Override 17 | public void onRequestExecute(final ApiResponse response) { 18 | System.out.println(gson.toJson(response)); 19 | } 20 | 21 | private class ChatTypeSerializer implements JsonSerializer { 22 | @Override 23 | public JsonElement serialize(final ChatType src, final Type typeOfSrc, final JsonSerializationContext context) { 24 | return new JsonPrimitive(src.getApiValue()); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /sample/src/main/java/ru/mail/im/botapi/sample/SimpleStart.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.sample; 2 | 3 | import ru.mail.im.botapi.BotApiClient; 4 | import ru.mail.im.botapi.BotApiClientController; 5 | import ru.mail.im.botapi.entity.ChatAction; 6 | 7 | import java.io.File; 8 | import java.io.IOException; 9 | import java.util.Scanner; 10 | 11 | public class SimpleStart { 12 | 13 | public static void main(String[] args) throws IOException { 14 | Scanner scanner = new Scanner(System.in); 15 | System.out.println("Enter bot token:"); 16 | String token = scanner.nextLine(); 17 | System.out.println("Enter test chatId:"); 18 | String chatId = scanner.nextLine(); 19 | 20 | BotApiClient client = new BotApiClient(token); 21 | 22 | BotApiClientController controller = BotApiClientController.startBot(client); 23 | System.out.println("STARTED"); 24 | client.addOnEventFetchListener(System.out::println); 25 | controller.sendActions(chatId, ChatAction.TYPING); 26 | long messageId = controller.sendTextMessage(chatId, "test").getMsgId(); 27 | controller.sendActions(chatId, ChatAction.TYPING); 28 | controller.editText(chatId, messageId, "EDITED TEST"); 29 | controller.replyText(chatId, "Reply msg", messageId); 30 | 31 | // create file 1.txt in project root 32 | // File file = new File("1.txt"); 33 | // controller.sendFile(chatId, file); 34 | // controller.replyFile(chatId, file, messageId, "Awesome file"); 35 | 36 | 37 | while (!scanner.nextLine().equals("end")) { 38 | //do nothing here, just waiting for fetch 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /sample/src/main/java/ru/mail/im/botapi/sample/command/CommandHandler.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.sample.command; 2 | 3 | import ru.mail.im.botapi.entity.ChatAction; 4 | 5 | import java.io.File; 6 | 7 | public interface CommandHandler { 8 | void onStart(String token); 9 | 10 | void onExit(); 11 | 12 | void onSelf(); 13 | 14 | void onSendText(String chatId, String text); 15 | 16 | void onSendFile(String chatId, File file); 17 | 18 | void onSendFile(String chatId, File file, String caption); 19 | 20 | void onSendVoice(String chatId, File file); 21 | 22 | void onEditText(String chatId, long msgId, String newText); 23 | 24 | void onDelete(String chatId, long msgId); 25 | 26 | void onDelete(String chatId, long[] msgIds); 27 | 28 | void onSleep(long ms); 29 | 30 | void onChatAction(String chatId, ChatAction... actions); 31 | 32 | void onGetChatInfo(String chatId); 33 | 34 | void onGetChatAdmins(String chatId); 35 | 36 | void onFetch(File toFile); 37 | } 38 | -------------------------------------------------------------------------------- /sample/src/main/java/ru/mail/im/botapi/sample/command/CommandProcessor.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.sample.command; 2 | 3 | import ru.mail.im.botapi.entity.ChatAction; 4 | 5 | import java.io.*; 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | import java.util.Scanner; 9 | import java.util.regex.Matcher; 10 | import java.util.regex.Pattern; 11 | 12 | public class CommandProcessor { 13 | 14 | private final Pattern paramsPattern = Pattern.compile("(\\w+)\\s*=\\s*\"([^\"]*)\""); 15 | private final Pattern placeholderPattern = Pattern.compile("\\$\\{([^}]*)}"); 16 | 17 | private final Scanner input; 18 | private final Writer output; 19 | private final CommandHandler handler; 20 | 21 | private boolean running; 22 | private PlaceholderValueProvider placeholderValueProvider; 23 | 24 | public CommandProcessor(final InputStream input, final OutputStream output, final CommandHandler handler) { 25 | this.input = new Scanner(input); 26 | this.output = new OutputStreamWriter(output); 27 | this.handler = handler; 28 | } 29 | 30 | public void setPlaceholderValueProvider(final PlaceholderValueProvider placeholderValueProvider) { 31 | this.placeholderValueProvider = placeholderValueProvider; 32 | } 33 | 34 | public void start() throws IOException { 35 | running = true; 36 | print("Enter commands for bot matching (command param=\"value\" param2=\"value2\")"); 37 | print("For new bot print: start=\"token\""); 38 | print("Note a bot can only reply after the user has added it to his contact list, or if the user was the first to start a dialogue."); 39 | while (running) { 40 | printPrompt(); 41 | processInput(input.next()); 42 | } 43 | } 44 | 45 | private void processInput(final String command) throws IOException { 46 | if (command.startsWith("#")) { 47 | skipInputToLineEnd(); 48 | } else { 49 | processCommand(command); 50 | } 51 | print("OK"); 52 | } 53 | 54 | private void processCommand(final String command) { 55 | final Map params = readParams(); 56 | switch (command) { 57 | case "exit": { 58 | handler.onExit(); 59 | running = false; 60 | break; 61 | } 62 | case "sleep": { 63 | handler.onSleep(Long.parseLong(params.get("ms"))); 64 | break; 65 | } 66 | case "start": { 67 | handler.onStart(params.get("token")); 68 | break; 69 | } 70 | case "self": { 71 | handler.onSelf(); 72 | break; 73 | } 74 | case "send": { 75 | final String chatId = params.get("to"); 76 | if (params.containsKey("text")) { 77 | handler.onSendText(chatId, params.get("text")); 78 | } else if (params.containsKey("file")) { 79 | if (params.containsKey("caption")) { 80 | handler.onSendFile(chatId, new File(params.get("file")), params.get("caption")); 81 | } else { 82 | handler.onSendFile(chatId, new File(params.get("file"))); 83 | } 84 | } else if (params.containsKey("voice")) { 85 | handler.onSendVoice(chatId, new File(params.get("voice"))); 86 | } 87 | break; 88 | } 89 | case "edit": { 90 | handler.onEditText(params.get("chat"), Long.parseLong(params.get("msg")), params.get("text")); 91 | break; 92 | } 93 | case "delMsg": { 94 | final String chatId = params.get("chat"); 95 | if (params.containsKey("msg")) { 96 | handler.onDelete(chatId, Long.parseLong(params.get("msg"))); 97 | } else if (params.containsKey("msgs")) { 98 | final String[] list = params.get("msgs").split(","); 99 | final long[] ids = new long[list.length]; 100 | for (int i = 0; i < list.length; i++) { 101 | ids[i] = Long.parseLong(list[i]); 102 | } 103 | handler.onDelete(chatId, ids); 104 | } 105 | break; 106 | } 107 | case "notify": { 108 | final String chatId = params.get("chat"); 109 | if (params.containsKey("action")) { 110 | handler.onChatAction(chatId, ChatAction.valueOf(params.get("action"))); 111 | } else if (params.containsKey("actions")) { 112 | final String[] list = params.get("actions").split(","); 113 | final ChatAction[] actions = new ChatAction[list.length]; 114 | for (int i = 0; i < list.length; i++) { 115 | actions[i] = ChatAction.valueOf(list[i]); 116 | } 117 | handler.onChatAction(chatId, actions); 118 | } else { 119 | handler.onChatAction(chatId); 120 | } 121 | break; 122 | } 123 | case "getInfo": { 124 | handler.onGetChatInfo(params.get("chat")); 125 | break; 126 | } 127 | case "getAdmins": { 128 | handler.onGetChatAdmins(params.get("chat")); 129 | break; 130 | } 131 | case "fetch": { 132 | final File to = new File(params.get("to")); 133 | handler.onFetch(to); 134 | break; 135 | } 136 | } 137 | } 138 | 139 | private Map readParams() { 140 | final String text = input.nextLine(); 141 | final Matcher matcher = paramsPattern.matcher(text); 142 | final Map params = new HashMap<>(); 143 | while (matcher.find()) { 144 | params.put(matcher.group(1), replacePlaceholders(matcher.group(2))); 145 | } 146 | return params; 147 | } 148 | 149 | private String replacePlaceholders(String value) { 150 | final Matcher matcher = placeholderPattern.matcher(value); 151 | while (matcher.find()) { 152 | value = value.replace(matcher.group(0), placeholderValueProvider.provide(matcher.group(1))); 153 | } 154 | return value; 155 | } 156 | 157 | private void skipInputToLineEnd() { 158 | input.nextLine(); 159 | } 160 | 161 | private void printPrompt() throws IOException { 162 | output.write("$bot>"); 163 | output.flush(); 164 | } 165 | 166 | private void print(final String message) throws IOException { 167 | output.write(message); 168 | output.write(System.lineSeparator()); 169 | output.flush(); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /sample/src/main/java/ru/mail/im/botapi/sample/command/PlaceholderValueProvider.java: -------------------------------------------------------------------------------- 1 | package ru.mail.im.botapi.sample.command; 2 | 3 | public interface PlaceholderValueProvider { 4 | String provide(final String name); 5 | } 6 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'botapi-client' 2 | 3 | include 'library' 4 | include 'sample' --------------------------------------------------------------------------------