├── .gitignore ├── LICENSE ├── README.md ├── actor-bots-example ├── build.gradle ├── scripts │ └── deb │ │ ├── bin │ │ └── actor-bots │ │ └── preInstall.sh └── src │ └── main │ └── java │ └── im │ └── actor │ └── bots │ ├── ExampleBots.kt │ ├── MainBotFarm.kt │ └── MainBotFarmDebug.kt ├── actor-bots ├── build.gradle └── src │ └── main │ ├── java │ └── im │ │ └── actor │ │ └── bots │ │ ├── blocks │ │ ├── Notification.kt │ │ └── OAuth2.kt │ │ └── framework │ │ ├── MagicBot.kt │ │ ├── MagicBotEntities.kt │ │ ├── MagicBotFarm.kt │ │ ├── i18n │ │ ├── I18NEngine.java │ │ └── Strings.java │ │ ├── parser │ │ ├── MessageCommand.java │ │ ├── MessageText.java │ │ ├── ParsedMessage.java │ │ └── ParsingUtils.java │ │ ├── persistence │ │ ├── KotlinExtensions.kt │ │ ├── MagicBotPersistence.kt │ │ └── ServerKeyValue.java │ │ ├── stateful │ │ ├── Expect.kt │ │ ├── ExpectCommands.kt │ │ ├── ExpectInput.kt │ │ ├── ExpectRaw.kt │ │ └── MagicBotStateful.kt │ │ └── traits │ │ ├── APITrait.kt │ │ ├── AdminTrait.kt │ │ ├── AiTrait.kt │ │ ├── BugSnagtrait.kt │ │ ├── DispatcherTrait.kt │ │ ├── HTTPTrait.kt │ │ ├── I18NTrait.kt │ │ ├── LogTrait.kt │ │ ├── ParseTrait.kt │ │ └── SMTPTrait.kt │ └── resources │ ├── BotFather.properties │ ├── BotFather_Ru.properties │ ├── BotFather_Zn.properties │ └── reference.conf ├── build.gradle ├── docs ├── README.md ├── api │ ├── API.md │ ├── HTTP.md │ ├── I18N.md │ ├── admin.md │ ├── ai.md │ ├── key-value-local.md │ └── key-value-server.md ├── assets │ └── Actor_Logo.png └── tutorials │ ├── bot-about.md │ ├── bot-farm.md │ ├── bot-implement.md │ ├── bot-messages.md │ ├── bot-overlord.md │ ├── bot-persistent.md │ ├── bot-register.md │ ├── bot-stateful.md │ └── web-hooks.md ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | local.properties 2 | *.iml 3 | .idea 4 | actor-bots-example/build 5 | actor-bots/build 6 | .gradle 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 |
5 | Actor 6 |
7 |

8 |

9 | Platform • 10 | Bootstrap • 11 | Bots 12 |

13 | ------- 14 | 15 | # Actor Bot Platform 16 | 17 | Actor Bot platfrom allows you to implement your own bots for Actor with various modules that help you easily integrate with external services, store bot's state in built-in key-value storage or even implement your Siri-like bot with [api.ai](https://api.ai/) integration. It is based on [Kotlin language](https://kotlinlang.org). 18 | 19 | Features 20 | ============ 21 | | Features 22 | --------------------------|------------------------------------------------------------ 23 | :rocket: | Fast start with bot development 24 | :wrench: | Rich framework for building bots 25 | :squirrel: | Built-in Natural Language Processing with [api.ai](https://api.ai) 26 | :computer: | Easy to host anywhere 27 | :octocat: | Community supported 28 | 29 | ## Getting Started 30 | 31 | All documentation and tutorials are at [docs](docs) directory. 32 | 33 | ## Example Bot 34 | 35 | ```kotlin 36 | 37 | import im.actor.bots.framework.* 38 | 39 | class EchoBot(scope: MagicForkScope) : MagicBotFork(scope) { 40 | 41 | override fun onMessage(message: MagicBotMessage) { 42 | when (message) { 43 | is MagicBotTextMessage -> { 44 | sendText("Received: ${message.text}") 45 | } 46 | } 47 | } 48 | } 49 | 50 | fun main(args: Array) { 51 | farm("BotFarm") { 52 | bot(EchoBot::class) { 53 | name = "echo" 54 | token = "" 55 | } 56 | } 57 | } 58 | ``` 59 | 60 | ## Community 61 | 62 | You can reach Actor community in our [Actor Open Source group](https://actor.im/oss). 63 | 64 | ## License 65 | 66 | Licensed under [Apache 2.0](LICENSE) 67 | -------------------------------------------------------------------------------- /actor-bots-example/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.0.1-2' 3 | repositories { 4 | jcenter() 5 | mavenCentral() 6 | } 7 | dependencies { 8 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 9 | classpath 'com.netflix.nebula:gradle-ospackage-plugin:3.1.0' 10 | } 11 | } 12 | 13 | repositories { 14 | jcenter() 15 | mavenCentral() 16 | } 17 | 18 | apply plugin: 'java' 19 | apply plugin: 'kotlin' 20 | apply plugin: 'application' 21 | apply plugin: 'nebula.deb' 22 | 23 | group 'im.actor' 24 | version '1.0-SNAPSHOT' 25 | 26 | sourceCompatibility = 1.8 27 | 28 | mainClassName = "im.actor.bots.MainBotFarmKt" 29 | 30 | dependencies { 31 | 32 | compile project(':actor-bots') 33 | 34 | compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 35 | 36 | testCompile group: 'junit', name: 'junit', version: '4.11' 37 | } 38 | 39 | sourceSets { 40 | main.java.srcDirs += 'src/main/kotlin' 41 | } 42 | 43 | task actorBotsDeb(type: Deb) { 44 | release '1' 45 | 46 | into "/usr/lib/actor-bots" 47 | 48 | user 'actor' 49 | 50 | dependsOn installApp 51 | 52 | preInstall file('scripts/deb/preInstall.sh') 53 | 54 | from(jar.outputs.files) { 55 | into '/usr/lib/actor-bots' 56 | } 57 | from(configurations.runtime) { 58 | into '/usr/lib/actor-bots' 59 | } 60 | from('scripts/deb/bin') { 61 | into '/usr/bin' 62 | fileMode 0555 63 | } 64 | from('src/main/resources') { 65 | into '/usr/lib/actor-bots' 66 | } 67 | from('home') { 68 | createDirectoryEntry = true 69 | fileMode 0500 70 | into 'home' 71 | } 72 | } -------------------------------------------------------------------------------- /actor-bots-example/scripts/deb/bin/actor-bots: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | DEFAULT_JVM_OPTS="-Xmx64m" 4 | 5 | die ( ) { 6 | echo 7 | echo "$*" 8 | echo 9 | exit 1 10 | } 11 | 12 | # Determine the Java command to use to start the JVM. 13 | if [ -n "$JAVA_HOME" ] ; then 14 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 15 | # IBM's JDK on AIX uses strange locations for the executables 16 | JAVACMD="$JAVA_HOME/jre/sh/java" 17 | else 18 | JAVACMD="$JAVA_HOME/bin/java" 19 | fi 20 | if [ ! -x "$JAVACMD" ] ; then 21 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 22 | Please set the JAVA_HOME variable in your environment to match the 23 | location of your Java installation." 24 | fi 25 | else 26 | JAVACMD="java" 27 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | Please set the JAVA_HOME variable in your environment to match the 29 | location of your Java installation." 30 | fi 31 | 32 | ARGS="$@" 33 | NEW_ARGS[0]='' 34 | IDX=0 35 | for ARG in "$@"; do 36 | case $ARG in 37 | '-D'*) 38 | JVM_OPTS="$JVM_OPTS $ARG" 39 | ;; 40 | *) 41 | NEW_ARGS[$IDX]="$ARG" 42 | let IDX=$IDX+1 43 | ;; 44 | esac 45 | done 46 | ARGS="${NEW_ARGS[@]}" 47 | 48 | function splitJvmOpts() { 49 | JVM_OPTS=("$@") 50 | } 51 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JVM_OPTS 52 | 53 | exec "$JAVACMD" "${JVM_OPTS[@]}" -cp "/usr/lib/actor-bots/*" im.actor.bots.MainBotFarmKt $ARGS -------------------------------------------------------------------------------- /actor-bots-example/scripts/deb/preInstall.sh: -------------------------------------------------------------------------------- 1 | export USER=actor 2 | export GROUP=actor 3 | 4 | # check that owner group exists 5 | if [ -z "$(getent group ${GROUP})" ]; then 6 | groupadd ${GROUP} 7 | fi 8 | 9 | # check that user exists 10 | if [ -z "$(getent passwd ${USER})" ]; then 11 | useradd --gid ${GROUP} ${USER} 12 | fi 13 | 14 | # (optional) check that user belongs to group 15 | if ! id -G -n ${USER} | grep -qF ${GROUP} ; then 16 | usermod -a -G ${GROUP} ${USER} 17 | fi 18 | -------------------------------------------------------------------------------- /actor-bots-example/src/main/java/im/actor/bots/ExampleBots.kt: -------------------------------------------------------------------------------- 1 | package im.actor.bots 2 | 3 | import im.actor.bots.framework.MagicBotFork 4 | import im.actor.bots.framework.MagicBotMessage 5 | import im.actor.bots.framework.MagicBotTextMessage 6 | import im.actor.bots.framework.MagicForkScope 7 | import im.actor.bots.framework.persistence.MagicPersistentBot 8 | import im.actor.bots.framework.stateful.MagicStatefulBot 9 | import im.actor.bots.framework.stateful.isText 10 | import im.actor.bots.framework.stateful.oneShot 11 | import im.actor.bots.framework.stateful.text 12 | import org.json.JSONObject 13 | 14 | /** 15 | * Very simple echo bot that forwards message 16 | */ 17 | class EchoBot(scope: MagicForkScope) : MagicBotFork(scope) { 18 | 19 | override fun onMessage(message: MagicBotMessage) { 20 | when (message) { 21 | is MagicBotTextMessage -> { 22 | sendText("Received: ${message.text}") 23 | } 24 | } 25 | } 26 | } 27 | 28 | class EchoStatefulBot(scope: MagicForkScope) : MagicStatefulBot(scope) { 29 | override fun configure() { 30 | // Configure group behaviour 31 | ownNickname = "echo" 32 | enableInGroups = true 33 | onlyWithMentions = false 34 | 35 | oneShot("/start") { 36 | sendText("Hi, i'm simple echo bot, send me text and i'll send it back.") 37 | } 38 | 39 | oneShot("default") { 40 | if (isText) { 41 | sendText(text) 42 | } 43 | } 44 | 45 | } 46 | 47 | } 48 | 49 | /** 50 | * Echo persistent bot that keeps it's state between restart 51 | */ 52 | class EchoPersistentBot(scope: MagicForkScope) : MagicPersistentBot(scope) { 53 | 54 | var receivedCount: Int = 0 55 | 56 | override fun onRestoreState(state: JSONObject) { 57 | receivedCount = state.optInt("counter", 0) 58 | } 59 | 60 | override fun onMessage(message: MagicBotMessage) { 61 | sendText("Received ${receivedCount++} messages") 62 | } 63 | 64 | override fun onSaveState(state: JSONObject) { 65 | state.put("counter", receivedCount) 66 | } 67 | } -------------------------------------------------------------------------------- /actor-bots-example/src/main/java/im/actor/bots/MainBotFarm.kt: -------------------------------------------------------------------------------- 1 | package im.actor.bots 2 | 3 | import im.actor.bots.framework.farm 4 | import im.actor.bots.framework.traits.sharedBugSnagClient 5 | 6 | fun main(args: Array) { 7 | 8 | 9 | farm("NewFarm") { 10 | 11 | 12 | bot(EchoStatefulBot::class) { 13 | name = "BOT_NAME_HERE" 14 | token = "YOUR_TOKEN_HERE" 15 | } 16 | 17 | } 18 | } -------------------------------------------------------------------------------- /actor-bots-example/src/main/java/im/actor/bots/MainBotFarmDebug.kt: -------------------------------------------------------------------------------- 1 | package im.actor.bots 2 | 3 | import im.actor.bots.framework.farm 4 | 5 | fun main(args: Array) { 6 | farm("bots") { 7 | 8 | // Stewie 9 | // bot(WunderListDebugBot::class) { 10 | // name = "sample" 11 | // token = "b741a02405054df6104d83452db0170b61267c19" 12 | // overlordClazz = WunderListOverlord::class.java 13 | // } 14 | 15 | // Rabbit MQ 16 | // bot(RabbitMQBot::class) { 17 | // name = "rabbitmq" 18 | // token = "8a5e8010ce1131abe7d5928e382fec2b0db0d78b" 19 | // } 20 | 21 | // bot(StickerBot::class) { 22 | // name = "sample" 23 | // token = "b741a02405054df6104d83452db0170b61267c19" 24 | // } 25 | 26 | // bots(JarvisBot::class) { 27 | // name = "jarvis" 28 | // token = "00720484d06cc4b77d1eb17033bd36226f5ec339" 29 | // } 30 | } 31 | } -------------------------------------------------------------------------------- /actor-bots/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | jcenter() 4 | mavenCentral() 5 | } 6 | dependencies { 7 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.0.1-2" 8 | } 9 | } 10 | 11 | repositories { 12 | jcenter() 13 | mavenCentral() 14 | } 15 | 16 | apply plugin: 'java' 17 | apply plugin: 'kotlin' 18 | 19 | group 'im.actor' 20 | version '1.0-SNAPSHOT' 21 | 22 | sourceCompatibility = 1.8 23 | 24 | sourceSets { 25 | main.java.srcDirs += 'src/main/kotlin' 26 | } 27 | 28 | dependencies { 29 | compile 'im.actor:actor-botkit:1.0.67' 30 | compile 'im.actor:shardakka_2.11:0.1.16' 31 | compile 'org.iq80.leveldb:leveldb:0.7' 32 | compile 'org.fusesource.leveldbjni:leveldbjni-all:1.8' 33 | compile 'org.apache.httpcomponents:httpclient:4.5.1' 34 | compile 'org.json:json:20150729' 35 | compile 'org.jdom:jdom2:2.0.6' 36 | compile 'com.bugsnag:bugsnag:1.2.8' 37 | compile 'org.codemonkey.simplejavamail:simple-java-mail:2.4+' 38 | 39 | compile "org.jetbrains.kotlin:kotlin-stdlib:1.0.1-2" 40 | runtime "org.jetbrains.kotlin:kotlin-reflect:1.0.1-2" 41 | compile 'com.fasterxml.jackson.module:jackson-module-kotlin:2.6.5-2' 42 | 43 | compile 'commons-io:commons-io:2.4' 44 | compile 'commons-validator:commons-validator:1.4.1' 45 | 46 | testCompile group: 'junit', name: 'junit', version: '4.11' 47 | } -------------------------------------------------------------------------------- /actor-bots/src/main/java/im/actor/bots/blocks/Notification.kt: -------------------------------------------------------------------------------- 1 | package im.actor.bots.blocks 2 | 3 | import im.actor.bots.framework.* 4 | import im.actor.bots.framework.stateful.* 5 | import org.json.JSONArray 6 | import org.json.JSONObject 7 | import java.util.* 8 | 9 | open class NotificationBot(scope: MagicForkScope) : MagicStatefulBot(scope) { 10 | 11 | var welcomeMessage = "Hello! I am notification bots and i can send you various notifications. " + 12 | "Just send me /subscribe and i will start to broadcast messages to you" 13 | 14 | override fun configure() { 15 | 16 | oneShot("/start") { 17 | if (isAdmin(scope.sender)) { 18 | sendText("Hello! I can help you distribute notifications to people. " + 19 | "They just need to send me message '/subscribe' to subscribe to notifications " + 20 | "and i will broadcast it to them.\n" + 21 | "I have following commands:\n" + 22 | "- */hook_url* - Get web hook url\n" + 23 | "- */send* - Broadcast message\n" + 24 | "- */subscribe_admin* - Subscribing to admin events\n" + 25 | "- */unsubscribe_admin* - unsubscribing to admin events\n") 26 | } else { 27 | sendText(welcomeMessage) 28 | } 29 | } 30 | 31 | oneShot("/subscribe") { 32 | sendText("Congratulations! You have successfully *subscribed* to my notifications! To unsubscribe send /unsubscribe") 33 | sendToOverlord(NotificationOverlord.Subscribe(scope.peer)) 34 | } 35 | 36 | oneShot("/unsubscribe") { 37 | sendText("You have *unsubscribed* from my notifications. Feel free to subscribe again with /subscribe command") 38 | sendToOverlord(NotificationOverlord.Unsubscribe(scope.peer)) 39 | } 40 | 41 | oneShot("/subscribe_admin") { 42 | sendText("Congratulations! You have successfully *subscribed* to admin notifications! To unsubscribe send /unsubscribe_admin") 43 | sendToOverlord(NotificationOverlord.SubscribeAdmin(scope.peer)) 44 | } 45 | 46 | oneShot("/unsubscribe_admin") { 47 | sendText("You have *unsubscribed* from admin notifications. Feel free to subscribe again with /subscribe_admin command") 48 | sendToOverlord(NotificationOverlord.UnsubscribeAdmin(scope.peer)) 49 | } 50 | 51 | oneShot("/hook_url") { 52 | if (isSenderAdmin()) { 53 | var hook = scope.botKeyValue.getStringValue("notification_url") 54 | if (hook == null) { 55 | hook = createHook("notification_hook_url") 56 | scope.botKeyValue.setStringValue("notification_url", hook) 57 | } 58 | sendText("Notification hook is: *$hook*") 59 | } else { 60 | sendText("You is not allowed to do this") 61 | } 62 | } 63 | 64 | raw("/send") { 65 | 66 | var broadcastMessage: String? = null 67 | 68 | before { 69 | sendText("What do you want to broadcast?") 70 | goto("message") 71 | } 72 | 73 | expectInput("message") { 74 | received { 75 | broadcastMessage = text 76 | sendText("Success. Are you sure want to send message $broadcastMessage? Send yes or no in response.") 77 | goto("confirm") 78 | } 79 | validate { 80 | if (!isText) { 81 | sendText("Please, send valid text message") 82 | return@validate false 83 | } 84 | return@validate true 85 | } 86 | } 87 | 88 | expectInput("confirm") { 89 | received { 90 | when (text.toLowerCase()) { 91 | "yes" -> { 92 | sendToOverlord(NotificationOverlord.DoBroadcast(broadcastMessage!!)) 93 | sendText("Message sent!") 94 | goto("main") 95 | } 96 | "no" -> { 97 | sendText("Message send cancelled.") 98 | goto("main") 99 | } 100 | 101 | } 102 | } 103 | validate { 104 | if (isText) { 105 | when (text.toLowerCase()) { 106 | "yes" -> { 107 | return@validate true 108 | } 109 | "no" -> { 110 | return@validate true 111 | } 112 | } 113 | } 114 | sendText("Please, send yes or no.") 115 | return@validate false 116 | } 117 | } 118 | } 119 | } 120 | 121 | fun isSenderAdmin(): Boolean { 122 | if (scope.sender == null) { 123 | return false 124 | } 125 | val sender = getUser(scope.sender!!.id()) 126 | if (sender.username.isPresent && admins.contains(sender.username.get())) { 127 | return true 128 | } 129 | return false 130 | } 131 | } 132 | 133 | class NotificationOverlord(scope: MagicOverlordScope) : MagicOverlord(scope) { 134 | 135 | val keyValue = scope.botKeyValue 136 | val subscribers = ArrayList() 137 | val adminSubscribers = ArrayList() 138 | 139 | // Events 140 | 141 | fun onText(text: String) { 142 | for (s in subscribers) { 143 | sendText(s, text) 144 | } 145 | 146 | onAdminText("Broadcasted message\n$text") 147 | } 148 | 149 | fun onAdminText(text: String) { 150 | for (s in adminSubscribers) { 151 | sendText(s, text) 152 | } 153 | } 154 | 155 | fun onSubscribe(peer: OutPeer) { 156 | if (subscribers.contains(peer)) { 157 | return 158 | } 159 | subscribers.add(peer) 160 | saveSubscribers() 161 | 162 | onAdminText("Subscribed $peer") 163 | } 164 | 165 | fun onUnsubscribe(peer: OutPeer) { 166 | subscribers.remove(peer) 167 | saveSubscribers() 168 | 169 | onAdminText("Unsubscribed $peer") 170 | } 171 | 172 | fun onSubscribeAdmin(peer: OutPeer) { 173 | if (adminSubscribers.contains(peer)) { 174 | return 175 | } 176 | adminSubscribers.add(peer) 177 | saveSubscribers() 178 | } 179 | 180 | fun onUnsubscribeAdmin(peer: OutPeer) { 181 | adminSubscribers.remove(peer) 182 | saveSubscribers() 183 | } 184 | 185 | fun saveSubscribers() { 186 | val storage = JSONObject() 187 | 188 | val peers = JSONArray() 189 | for (s in subscribers) { 190 | peers.put(s.toJson()) 191 | } 192 | storage.put("peers", peers) 193 | 194 | val adminPeers = JSONArray() 195 | for (s in adminSubscribers) { 196 | adminPeers.put(s.toJson()) 197 | } 198 | storage.put("adminPeers", adminPeers) 199 | 200 | keyValue.setStringValue("storage", storage.toString()) 201 | } 202 | 203 | fun loadSubscribers() { 204 | subscribers.clear() 205 | adminSubscribers.clear() 206 | try { 207 | val storage = JSONObject(keyValue.getStringValue("storage")) 208 | val peers = storage.getJSONArray("peers") 209 | for (i in 0..peers.length()) { 210 | try { 211 | subscribers.add(outPeerFromJson(peers.getJSONObject(i))) 212 | } catch(e: Exception) { 213 | e.printStackTrace() 214 | } 215 | } 216 | val adminPeers = storage.getJSONArray("adminPeers") 217 | for (i in 0..adminPeers.length()) { 218 | try { 219 | adminSubscribers.add(outPeerFromJson(peers.getJSONObject(i))) 220 | } catch(e: Exception) { 221 | e.printStackTrace() 222 | } 223 | } 224 | } catch(e: Exception) { 225 | e.printStackTrace() 226 | } 227 | } 228 | 229 | // Processor 230 | 231 | override fun preStart() { 232 | super.preStart() 233 | 234 | loadSubscribers() 235 | } 236 | 237 | override fun onReceive(update: Any?) { 238 | if (update is Subscribe) { 239 | onSubscribe(update.peer) 240 | } else if (update is Unsubscribe) { 241 | onUnsubscribe(update.peer) 242 | } else if (update is SubscribeAdmin) { 243 | onSubscribeAdmin(update.peer) 244 | } else if (update is UnsubscribeAdmin) { 245 | onUnsubscribeAdmin(update.peer) 246 | } else if (update is DoBroadcast) { 247 | onText(update.message) 248 | } else { 249 | super.onReceive(update) 250 | } 251 | } 252 | 253 | override fun onWebHookReceived(hook: HookData) { 254 | if (hook.jsonBody != null) { 255 | val text = hook.jsonBody.optString("text") 256 | if (text != null) { 257 | onText(text) 258 | } 259 | } 260 | } 261 | 262 | data class DoBroadcast(val message: String) 263 | 264 | data class Subscribe(val peer: OutPeer) 265 | 266 | data class Unsubscribe(val peer: OutPeer) 267 | 268 | data class SubscribeAdmin(val peer: OutPeer) 269 | 270 | data class UnsubscribeAdmin(val peer: OutPeer) 271 | } -------------------------------------------------------------------------------- /actor-bots/src/main/java/im/actor/bots/blocks/OAuth2.kt: -------------------------------------------------------------------------------- 1 | package im.actor.bots.blocks 2 | 3 | import im.actor.bots.framework.* 4 | import im.actor.bots.framework.persistence.ServerKeyValue 5 | import im.actor.bots.framework.stateful.MagicStatefulBot 6 | import im.actor.bots.framework.stateful.oneShot 7 | import im.actor.bots.framework.traits.HTTPTrait 8 | import im.actor.bots.framework.traits.HTTPTraitImpl 9 | import org.json.JSONObject 10 | import java.util.* 11 | 12 | private val OAuth2WebHookName = "oauth_callback_url" 13 | private var apiCache = HashMap>() 14 | 15 | abstract class OAuth2Bot(scope: MagicForkScope) : MagicStatefulBot(scope) { 16 | 17 | // Used for generating random state string 18 | private val random = Random() 19 | 20 | override fun configure() { 21 | 22 | // Configuring built-in admin method 23 | if (isAdminScope()) { 24 | oneShot("/token") { 25 | sendText("OAuth2 Callback url is: " + getOAuthCallback()) 26 | } 27 | } 28 | 29 | if (scope.peer.isPrivate) { 30 | var api = apiForUid(scope.peer.id) 31 | oneShot("/login") { 32 | if (api.isAuthenticated()) { 33 | sendText("You is already authenticated. Send [/logout](send:/logout) to logout.") 34 | } else { 35 | sendText("Please, open url: ${api.authenticateUrl(registerOAuth2Request())}") 36 | } 37 | } 38 | oneShot("/logout") { 39 | if (!api.isAuthenticated()) { 40 | sendText("You is not authenticated. Please, send [/login](send:/login) to log in.") 41 | } else { 42 | api.revokeAuthentication() 43 | api.saveAuthState() 44 | sendText("You successfully logged out.") 45 | } 46 | } 47 | } else { 48 | oneShot("/login") { 49 | sendText("Please, do it in private") 50 | } 51 | oneShot("/logout") { 52 | sendText("Please, do it in private") 53 | } 54 | } 55 | } 56 | 57 | /** 58 | * Getting API for UID with built-in cache 59 | */ 60 | fun apiForUid(uid: Int): T { 61 | synchronized(apiCache) { 62 | var res = apiCache[scope.name] 63 | if (res != null) { 64 | val cached = res[uid] 65 | if (cached != null) { 66 | return cached as T 67 | } 68 | val resApi = createApi(uid) 69 | res.put(uid, resApi) 70 | return resApi 71 | } else { 72 | res = HashMap() 73 | val resApi = createApi(uid) 74 | res.put(uid, resApi) 75 | apiCache.put(scope.name, res) 76 | return resApi 77 | } 78 | } 79 | } 80 | 81 | /** 82 | * Create API for user with UID 83 | */ 84 | abstract fun createApi(uid: Int): T 85 | 86 | 87 | /** 88 | * Called when authentication successful 89 | */ 90 | open fun onAuthSuccess() { 91 | 92 | } 93 | 94 | /** 95 | * Called when authentication failed 96 | */ 97 | open fun onAuthFailure() { 98 | 99 | } 100 | 101 | // 102 | // Implementation 103 | // 104 | 105 | /** 106 | * Generating random state string 107 | */ 108 | fun randomState(): String { 109 | return Math.abs(random.nextLong()).toString() 110 | } 111 | 112 | /** 113 | * Implement this method for handling OAuth2 Response 114 | */ 115 | fun onOAuthResponse(code: String) { 116 | val api = apiForUid(scope.peer.id) 117 | if (api.authenticate(code)) { 118 | api.saveAuthState() 119 | onAuthSuccess() 120 | } else { 121 | onAuthFailure() 122 | } 123 | } 124 | 125 | /** 126 | * Registering OAuth2 request with internally generated state 127 | */ 128 | fun registerOAuth2Request(): String { 129 | val state = randomState() 130 | registerOAuth2Request(state) 131 | return state 132 | } 133 | 134 | /** 135 | * Registering OAuth2 request 136 | */ 137 | fun registerOAuth2Request(state: String) { 138 | if (!scope.peer.isPrivate) { 139 | throw RuntimeException("Can't register state not from private chat") 140 | } 141 | sendToOverlord(RegisterOAuth2State(state, scope.peer)) 142 | } 143 | 144 | /** 145 | * Get This OAuth2 Callback to entering to OAuth2 provider 146 | */ 147 | fun getOAuthCallback(): String { 148 | val cached = scope.botKeyValue.getStringValue("oauth_callback_url") 149 | if (cached != null) { 150 | return cached 151 | } 152 | val res = createHook(OAuth2WebHookName) ?: throw RuntimeException("Unable to register hook") 153 | scope.botKeyValue.setStringValue("oauth_callback_url", res) 154 | return res 155 | } 156 | 157 | override fun onOverlordMessage(message: Any) { 158 | when (message) { 159 | is OAuth2Result -> { 160 | onOAuthResponse(message.code) 161 | } 162 | } 163 | } 164 | } 165 | 166 | /** 167 | * Subclass from this overlord for providing OAuth2 support 168 | */ 169 | open class OAuth2Overlord(scope: MagicOverlordScope) : MagicOverlord(scope) { 170 | 171 | private val ids = HashMap() 172 | 173 | override fun onWebHookReceived(hook: HookData) { 174 | when (hook.name) { 175 | OAuth2WebHookName -> { 176 | var args = hook.query.split("&") 177 | var code: String? = null 178 | var state: String? = null 179 | for (a in args) { 180 | val parts = a.split("=") 181 | if (parts.size != 2) { 182 | continue 183 | } 184 | when (parts[0]) { 185 | "code" -> { 186 | code = parts[1] 187 | } 188 | "state" -> { 189 | state = parts[1] 190 | } 191 | } 192 | } 193 | 194 | if (code != null && state != null) { 195 | val peer = ids[state] 196 | if (peer != null) { 197 | sendToForks(peer, OAuth2Result(code)) 198 | } 199 | } 200 | } 201 | } 202 | } 203 | 204 | override fun onReceive(update: Any?) { 205 | when (update) { 206 | is RegisterOAuth2State -> { 207 | ids.put(update.state, update.peer) 208 | } 209 | else -> { 210 | super.onReceive(update) 211 | } 212 | } 213 | } 214 | } 215 | 216 | data class OAuth2Result(val code: String) 217 | data class RegisterOAuth2State(val state: String, val peer: OutPeer) 218 | 219 | abstract class OAuth2Api(val clientId: String, val clientSecret: String, val storage: ServerKeyValue, val uid: Int) : 220 | HTTPTrait by HTTPTraitImpl() { 221 | 222 | init { 223 | val auth = storage.getJSONValue("auth_$uid") 224 | if (auth != null) { 225 | loadAuthState(auth) 226 | } 227 | } 228 | 229 | abstract fun isAuthenticated(): Boolean 230 | 231 | abstract fun authenticate(authCode: String): Boolean 232 | 233 | abstract fun authenticateUrl(state: String): String 234 | 235 | abstract fun revokeAuthentication() 236 | 237 | fun saveAuthState() { 238 | var obj = JSONObject() 239 | saveAuthState(obj) 240 | storage.setJSONValue("auth_$uid", obj) 241 | } 242 | 243 | protected abstract fun saveAuthState(obj: JSONObject) 244 | 245 | protected abstract fun loadAuthState(obj: JSONObject) 246 | } -------------------------------------------------------------------------------- /actor-bots/src/main/java/im/actor/bots/framework/MagicBot.kt: -------------------------------------------------------------------------------- 1 | package im.actor.bots.framework 2 | 3 | import akka.actor.ActorRef 4 | import akka.actor.Props 5 | import akka.actor.UntypedActor 6 | import im.actor.botkit.RemoteBot 7 | import im.actor.bots.BotMessages 8 | import im.actor.bots.framework.parser.MessageCommand 9 | import im.actor.bots.framework.parser.ParsedMessage 10 | import im.actor.bots.framework.persistence.ServerKeyValue 11 | import im.actor.bots.framework.traits.* 12 | import org.json.JSONObject 13 | import scala.Option 14 | import scala.concurrent.Future 15 | import java.nio.charset.Charset 16 | import java.util.* 17 | 18 | /** 19 | * Configuration of Bot 20 | * @param name Unique name of Bot 21 | * @param clazz Class of bots 22 | * @param overlordClazz Optional class of bots's overlord 23 | * @param token Bot's token in Actor Platform 24 | * @param endpoint Bot's API Endpoint 25 | * @param traceHook Optional WebHook for logging 26 | */ 27 | class MagicBotConfig(val name: String, 28 | val clazz: Class<*>, 29 | val overlordClazz: Class<*>?, 30 | val token: String, 31 | val endpoint: String, 32 | val traceHook: String?) { 33 | 34 | } 35 | 36 | /** 37 | * Bot's Scope: Context information for bots 38 | * @param name Unique name of bots 39 | * @param peer Fork's peer 40 | * @param sender Sender of message 41 | * @param bot Magical Remote Bot, required to make requests 42 | * @param forkKeyValue Fork's key-value. Useful for storing conversation state. 43 | * @param botKeyValue All bots's key value. Useful for storing bots's state. 44 | * @param overlord Optional Overlord's actor ref 45 | */ 46 | class MagicForkScope(val name: String, 47 | var peer: OutPeer, 48 | var sender: BotMessages.User?, 49 | val bot: MagicalRemoteBot, 50 | val forkKeyValue: ServerKeyValue, 51 | val botKeyValue: ServerKeyValue, 52 | val overlord: ActorRef?) { 53 | } 54 | 55 | /** 56 | * Overlord's scope 57 | * @param botKeyValue Bot's key value 58 | * @param bot Magical Remote Bot, required to make requests 59 | */ 60 | class MagicOverlordScope(val botKeyValue: ServerKeyValue, 61 | val rootRef: ActorRef, 62 | val bot: MagicalRemoteBot) 63 | 64 | /** 65 | * Magical Remote Bot 66 | * @param config Configuration for bots 67 | */ 68 | open class MagicalRemoteBot(val config: MagicBotConfig) : 69 | HTTPTrait by HTTPTraitImpl(), 70 | BugSnag by BugSnagImpl(), 71 | RemoteBot(config.token, config.endpoint) { 72 | 73 | private var child: ActorRef? = null 74 | 75 | override fun preStart() { 76 | super.preStart() 77 | child = context().actorOf(Props.create(MagicChildBot::class.java, this, config), "child") 78 | } 79 | 80 | /** 81 | * Handling New Message received 82 | */ 83 | override fun onMessage(p0: BotMessages.Message?) { 84 | 85 | child?.tell(p0, self()) 86 | 87 | // Tracing 88 | var message = "<<<<<< ${p0!!.peer().toUsable().toUniqueId()}" 89 | val sender = getUser(p0.sender().id()) 90 | if (sender.emailContactRecords.size > 0) { 91 | message += "\nby *${sender.name()}* (${sender.emailContactRecords.first().email()})" 92 | } else if (sender.phoneContactRecords.size > 0) { 93 | message += "\nby *${sender.name()}* (+${sender.phoneContactRecords.first().phone()})" 94 | } else { 95 | return 96 | } 97 | message += "\n```\n${p0.message()}```" 98 | trace(message) 99 | } 100 | 101 | /** 102 | * Handling Raw Update received 103 | */ 104 | override fun onRawUpdate(p0: BotMessages.RawUpdate?) { 105 | child?.tell(p0, self()) 106 | 107 | // Tracing 108 | trace("<<<<<< Update\n```\n$p0\n```") 109 | } 110 | 111 | override fun requestSendMessage(peer: BotMessages.OutPeer?, randomId: Long, message: BotMessages.MessageBody?): Future? { 112 | val res = super.requestSendMessage(peer, randomId, message) 113 | // Tracing 114 | trace(">>>>>> ($peer)\n```\n$message\n```") 115 | return res 116 | } 117 | 118 | private fun trace(msg: String) { 119 | if (config.traceHook != null) { 120 | urlPostJson(config.traceHook, Json.JsonObject(JSONObject().apply { 121 | put("text", msg) 122 | })) 123 | } 124 | } 125 | 126 | override fun preRestart(reason: Throwable?, message: Option?) { 127 | super.preRestart(reason, message) 128 | 129 | logException(reason) 130 | } 131 | 132 | /** 133 | * Child Bot to avoid dead locks in RPC requests 134 | */ 135 | class MagicChildBot(val bot: MagicalRemoteBot, val config: MagicBotConfig) : 136 | BugSnag by BugSnagImpl(), 137 | UntypedActor() { 138 | 139 | /** 140 | * Bot's key value storage 141 | */ 142 | private var botKeyValue = ServerKeyValue(bot) 143 | 144 | /** 145 | * Overlord for bots 146 | */ 147 | private var overlord: ActorRef? = null 148 | 149 | override fun preStart() { 150 | super.preStart() 151 | 152 | if (config.overlordClazz != null) { 153 | overlord = context().actorOf(Props.create(config.overlordClazz, MagicOverlordScope(botKeyValue, self(), bot)), "overlord") 154 | } 155 | } 156 | 157 | /** 158 | * Message received handler: Forwarding to specific fork 159 | */ 160 | final override fun onReceive(message: Any?) { 161 | when (message) { 162 | is BotMessages.Message -> { 163 | peerActor(message.peer().toUsable(), message.sender()).tell(message, self()) 164 | } 165 | is BotMessages.RawUpdate -> { 166 | overlord?.tell(message, self()) 167 | } 168 | is OverlordMessage -> { 169 | peerActor(message.peer, null).tell(message, self()) 170 | } 171 | } 172 | } 173 | 174 | /** 175 | * Building peer's Actor 176 | */ 177 | fun peerActor(peer: OutPeer, sender: BotMessages.UserOutPeer?): ActorRef { 178 | val peerId = peer.toUniqueId() 179 | val cached = context().child(peerId) 180 | if (cached.nonEmpty()) { 181 | return cached.get() 182 | } else { 183 | val scope = MagicForkScope(bot.config.name, peer, if (sender != null) bot.getUser(sender.id()) else null, bot, 184 | ServerKeyValue(bot, "peer_$peerId"), botKeyValue, overlord) 185 | return context().actorOf(Props.create(bot.config.clazz, scope), peerId) 186 | } 187 | } 188 | 189 | override fun preRestart(reason: Throwable?, message: Option?) { 190 | super.preRestart(reason, message) 191 | 192 | logException(reason) 193 | } 194 | } 195 | } 196 | 197 | private data class OverlordMessage(val peer: OutPeer, val message: Any) 198 | 199 | /** 200 | * Main Magic Bot 201 | */ 202 | abstract class MagicBotFork(val scope: MagicForkScope) : 203 | HTTPTrait by HTTPTraitImpl(), 204 | AiTrait by AiTraitImpl(), 205 | I18NTrait by I18NTraitImpl(), 206 | LogTrait by LogTraitImpl(), 207 | APITraitScoped by APITraitScopedImpl(scope.peer, scope.bot), 208 | AdminTraitScoped by AdminTraitScopedImpl(scope), 209 | DispatchTrait by DispatchTraitImpl(), 210 | BugSnag by BugSnagImpl(), 211 | UntypedActor() { 212 | 213 | /** 214 | * Enable bots handling in groups. 215 | */ 216 | var enableInGroups = true 217 | 218 | /** 219 | * Enable message handling only with mentions. 220 | */ 221 | var onlyWithMentions = true 222 | 223 | /** 224 | * Bot's nickname. Used for filtering messages. 225 | */ 226 | var ownNickname: String? = null 227 | 228 | /** 229 | * Constructing Bot 230 | */ 231 | init { 232 | initLog(this) 233 | initDispatch(this) 234 | } 235 | 236 | /** 237 | * Handling messages 238 | */ 239 | abstract fun onMessage(message: MagicBotMessage) 240 | 241 | /** 242 | * Handling overlord messages 243 | */ 244 | open fun onOverlordMessage(message: Any) { 245 | 246 | } 247 | 248 | /** 249 | * Called after onMessage. Useful for saving state. 250 | */ 251 | open fun afterMessage() { 252 | 253 | } 254 | 255 | final override fun onReceive(message: Any?) { 256 | 257 | if (message is OverlordMessage) { 258 | onOverlordMessage(message.message) 259 | return 260 | } 261 | 262 | var msg: MagicBotMessage 263 | 264 | when (message) { 265 | // 266 | // General messages 267 | // 268 | is BotMessages.Message -> { 269 | val content = message.message() 270 | when (content) { 271 | // 272 | // Handling Text Message 273 | // 274 | is BotMessages.TextMessage -> { 275 | 276 | // 277 | // Group message filtration 278 | // 279 | if (scope.peer.isGroup) { 280 | if (!enableInGroups) { 281 | return 282 | } 283 | if (onlyWithMentions) { 284 | if (ownNickname != null) { 285 | if (!content.text().toLowerCase().contains("@${ownNickname!!.toLowerCase()}")) { 286 | return 287 | } 288 | } else { 289 | return 290 | } 291 | } 292 | } 293 | 294 | // 295 | // Building Text Message 296 | // 297 | val mText = MagicBotTextMessage(message.peer().toUsable(), message.sender(), message.randomId(), 298 | content.text()) 299 | msg = mText 300 | 301 | // Setting Command parameters if needed 302 | val pMsg = ParsedMessage.matchType(content.text()) 303 | if (pMsg is MessageCommand) { 304 | mText.command = pMsg.command 305 | mText.commandArgs = pMsg.data 306 | } 307 | } 308 | // 309 | // Handling JSON messages 310 | // 311 | is BotMessages.JsonMessage -> { 312 | try { 313 | val pJson = JSONObject(content.rawJson()) 314 | msg = MagicBotJsonMessage(message.peer().toUsable(), message.sender(), 315 | message.randomId(), pJson) 316 | } catch(e: Exception) { 317 | e.printStackTrace() 318 | return 319 | } 320 | } 321 | // 322 | // Handling Document Messages 323 | // 324 | is BotMessages.DocumentMessage -> { 325 | msg = MagicBotDocMessage(message.peer().toUsable(), message.sender(), 326 | message.randomId(), content) 327 | } 328 | // 329 | // Handling Sticker Messages 330 | // 331 | is BotMessages.StickerMessage -> { 332 | msg = MagicBotStickerMessage(message.peer().toUsable(), message.sender(), 333 | message.randomId(), content) 334 | } 335 | // 336 | // Ignoring unknown message 337 | // 338 | else -> { 339 | return 340 | } 341 | } 342 | } 343 | else -> { 344 | return 345 | } 346 | } 347 | 348 | scope.peer = msg.peer 349 | scope.sender = if (msg.sender != null) getUser(msg.sender!!.id()) else null 350 | onMessage(msg) 351 | afterMessage() 352 | } 353 | 354 | /** 355 | * Sending message to Overlord 356 | */ 357 | fun sendToOverlord(message: Any) { 358 | scope.overlord?.tell(message, scope.bot.self()) 359 | } 360 | 361 | override fun preRestart(reason: Throwable?, message: Option?) { 362 | super.preRestart(reason, message) 363 | 364 | logException(reason) 365 | } 366 | } 367 | 368 | 369 | /** 370 | * Magic Bot Overlord. Actor that is used to receive various updates that is not connected 371 | * to specific conversation. 372 | */ 373 | abstract class MagicOverlord(val scope: MagicOverlordScope) : 374 | APITrait by APITraitImpl(scope.bot), 375 | LogTrait by LogTraitImpl(), 376 | BugSnag by BugSnagImpl(), 377 | DispatchTrait by DispatchTraitImpl(), 378 | UntypedActor() { 379 | 380 | init { 381 | initLog(this) 382 | initDispatch(this) 383 | } 384 | 385 | override fun preStart() { 386 | super.preStart() 387 | 388 | Thread.sleep(5000) 389 | 390 | val state = scope.botKeyValue.getJSONValue("overlord_state") 391 | if (state != null) { 392 | onRestoreState(state) 393 | } 394 | } 395 | 396 | open fun onRestoreState(state: JSONObject) { 397 | 398 | } 399 | 400 | open fun onSaveState(state: JSONObject) { 401 | 402 | } 403 | 404 | fun saveState() { 405 | val state = JSONObject() 406 | onSaveState(state) 407 | scope.botKeyValue.setJSONValue("overlord_state", state) 408 | } 409 | 410 | /** 411 | * Called when Web Hook are received 412 | * @param hook WebHook data 413 | */ 414 | abstract fun onWebHookReceived(hook: HookData) 415 | 416 | /** 417 | * Sending message to Fork 418 | * @param peer Peer for message 419 | * @param message Message to send 420 | */ 421 | fun sendToForks(peer: OutPeer, message: Any) { 422 | scope.rootRef.tell(OverlordMessage(peer, message), self()) 423 | } 424 | 425 | override fun onReceive(update: Any?) { 426 | when (update) { 427 | is BotMessages.RawUpdate -> { 428 | if (update.type.isPresent && update.type.get() == "HookData") { 429 | val res = Base64.getDecoder().decode(update.data()) 430 | val resS = String(res) 431 | val resJ = JSONObject(resS) 432 | if (resJ.getString("dataType") != "HookData") { 433 | return 434 | } 435 | val data = resJ.getJSONObject("data") 436 | val method = data.getString("method") 437 | val queryString = data.optString("queryString") 438 | val name = data.getString("name") 439 | val body = Base64.getDecoder().decode(data.getString("body")) 440 | val headers = data.getJSONObject("headers") 441 | 442 | var jsonBody: JSONObject? = null 443 | try { 444 | jsonBody = JSONObject(String(body)) 445 | } catch(e: Exception) { 446 | // Ignore 447 | } 448 | 449 | onWebHookReceived(HookData( 450 | name = name, 451 | method = method, 452 | query = queryString, 453 | body = body, 454 | jsonBody = jsonBody, 455 | headers = headers)) 456 | } 457 | } 458 | } 459 | } 460 | 461 | override fun preRestart(reason: Throwable?, message: Option?) { 462 | super.preRestart(reason, message) 463 | 464 | logException(reason) 465 | } 466 | } 467 | 468 | data class HookData(val name: String, val method: String, val query: String, val body: ByteArray, val jsonBody: JSONObject?, val headers: JSONObject) -------------------------------------------------------------------------------- /actor-bots/src/main/java/im/actor/bots/framework/MagicBotEntities.kt: -------------------------------------------------------------------------------- 1 | package im.actor.bots.framework 2 | 3 | import im.actor.bots.BotMessages 4 | import org.json.JSONObject 5 | 6 | // 7 | // Magic Bot Messages 8 | // 9 | 10 | public abstract class MagicBotMessage(val peer: OutPeer, val sender: BotMessages.UserOutPeer?, 11 | val rid: Long) { 12 | 13 | } 14 | 15 | public class MagicBotTextMessage(peer: OutPeer, sender: BotMessages.UserOutPeer?, rid: Long, 16 | val text: String) : MagicBotMessage(peer, sender, rid) { 17 | var command: String? = null 18 | var commandArgs: String? = null 19 | } 20 | 21 | public class MagicBotJsonMessage(peer: OutPeer, sender: BotMessages.UserOutPeer?, rid: Long, 22 | val json: JSONObject) : MagicBotMessage(peer, sender, rid) { 23 | 24 | } 25 | 26 | public class MagicBotDocMessage(peer: OutPeer, sender: BotMessages.UserOutPeer?, rid: Long, 27 | val doc: BotMessages.DocumentMessage) : MagicBotMessage(peer, sender, rid) { 28 | 29 | } 30 | 31 | public class MagicBotStickerMessage(peer: OutPeer, sender: BotMessages.UserOutPeer?, rid: Long, 32 | val sticker: BotMessages.StickerMessage) : MagicBotMessage(peer, sender, rid) { 33 | 34 | } 35 | 36 | // 37 | // User Extensions 38 | // 39 | 40 | var BotMessages.User.isEnterprise: Boolean 41 | get() { 42 | return this.emailContactRecords.size > 0 43 | } 44 | private set(v) { 45 | 46 | } 47 | 48 | // 49 | // Peers and OutPeers 50 | // 51 | 52 | public fun peerFromJson(json: JSONObject): Peer { 53 | val type = json.getString("type") 54 | when (type) { 55 | "group" -> { 56 | return Peer(PeerType.GROUP, json.getInt("id")) 57 | } 58 | "private" -> { 59 | return Peer(PeerType.PRIVATE, json.getInt("id")) 60 | } 61 | else -> { 62 | throw RuntimeException("Unknown type $type") 63 | } 64 | } 65 | } 66 | 67 | public class Peer(val type: PeerType, val id: Int) { 68 | 69 | var isGroup: Boolean 70 | get() { 71 | return type == PeerType.GROUP 72 | } 73 | private set(v) { 74 | } 75 | 76 | var isPrivate: Boolean 77 | get() { 78 | return type == PeerType.PRIVATE 79 | } 80 | private set(v) { 81 | } 82 | 83 | fun toJson(): JSONObject { 84 | val res = JSONObject() 85 | res.put("id", id) 86 | when (type) { 87 | PeerType.GROUP -> { 88 | res.put("type", "group") 89 | } 90 | PeerType.PRIVATE -> { 91 | res.put("type", "private") 92 | } 93 | } 94 | return res 95 | } 96 | 97 | fun toKit(): BotMessages.Peer { 98 | when (type) { 99 | PeerType.PRIVATE -> { 100 | return BotMessages.UserPeer(id) 101 | } 102 | PeerType.GROUP -> { 103 | return BotMessages.GroupPeer(id) 104 | } 105 | } 106 | } 107 | 108 | fun toUniqueId(): String { 109 | when (type) { 110 | PeerType.PRIVATE -> { 111 | return "PRIVATE_$id" 112 | } 113 | PeerType.GROUP -> { 114 | return "GROUP_$id" 115 | } 116 | } 117 | } 118 | 119 | override fun equals(other: Any?): Boolean { 120 | if (this === other) return true 121 | if (other?.javaClass != javaClass) return false 122 | 123 | other as Peer 124 | 125 | if (type != other.type) return false 126 | if (id != other.id) return false 127 | 128 | return true 129 | } 130 | 131 | override fun hashCode(): Int { 132 | var result = type.hashCode() 133 | result += 31 * result + id 134 | return result 135 | } 136 | } 137 | 138 | public fun outPeerFromJson(json: JSONObject): OutPeer { 139 | val type = json.getString("type") 140 | when (type) { 141 | "group" -> { 142 | return OutPeer(PeerType.GROUP, json.getInt("id"), json.getString("accessHash").toLong()) 143 | } 144 | "private" -> { 145 | return OutPeer(PeerType.PRIVATE, json.getInt("id"), json.getString("accessHash").toLong()) 146 | } 147 | else -> { 148 | throw RuntimeException("Unknown type $type") 149 | } 150 | } 151 | } 152 | 153 | public fun BotMessages.OutPeer.toUsable(): OutPeer { 154 | if (this is BotMessages.UserOutPeer) { 155 | return OutPeer(PeerType.PRIVATE, id(), accessHash()) 156 | } else if (this is BotMessages.GroupOutPeer) { 157 | return OutPeer(PeerType.GROUP, id(), accessHash()) 158 | } else { 159 | throw RuntimeException("Unknown type") 160 | } 161 | } 162 | 163 | public class OutPeer(val type: PeerType, val id: Int, val accessHash: Long) { 164 | 165 | var isGroup: Boolean 166 | get() { 167 | return type == PeerType.GROUP 168 | } 169 | private set(v) { 170 | } 171 | 172 | var isPrivate: Boolean 173 | get() { 174 | return type == PeerType.PRIVATE 175 | } 176 | private set(v) { 177 | } 178 | 179 | fun toJson(): JSONObject { 180 | val res = JSONObject() 181 | res.put("id", id) 182 | res.put("accessHash", "$accessHash") 183 | when (type) { 184 | PeerType.GROUP -> { 185 | res.put("type", "group") 186 | } 187 | PeerType.PRIVATE -> { 188 | res.put("type", "private") 189 | } 190 | } 191 | return res 192 | } 193 | 194 | fun toPeer(): Peer { 195 | return Peer(type, id) 196 | } 197 | 198 | fun toKit(): BotMessages.OutPeer { 199 | when (type) { 200 | PeerType.PRIVATE -> { 201 | return BotMessages.UserOutPeer(id, accessHash) 202 | } 203 | PeerType.GROUP -> { 204 | return BotMessages.GroupOutPeer(id, accessHash) 205 | } 206 | } 207 | } 208 | 209 | fun toUniqueId(): String { 210 | when (type) { 211 | PeerType.PRIVATE -> { 212 | return "PRIVATE_$id" 213 | } 214 | PeerType.GROUP -> { 215 | return "GROUP_$id" 216 | } 217 | } 218 | } 219 | 220 | override fun equals(other: Any?): Boolean { 221 | if (this === other) return true 222 | if (other?.javaClass != javaClass) return false 223 | 224 | other as OutPeer 225 | 226 | if (type != other.type) return false 227 | if (id != other.id) return false 228 | if (accessHash != other.accessHash) return false 229 | 230 | return true 231 | } 232 | 233 | override fun hashCode(): Int { 234 | var result = type.hashCode() 235 | result += 31 * result + id 236 | result += 31 * result + accessHash.hashCode() 237 | return result 238 | } 239 | } 240 | 241 | public enum class PeerType(val id: Int) { 242 | PRIVATE(0), GROUP(1) 243 | } 244 | 245 | 246 | public fun BotMessages.Peer.toUsable(): Peer { 247 | if (this is BotMessages.UserPeer) { 248 | return Peer(PeerType.PRIVATE, id()) 249 | } else if (this is BotMessages.GroupPeer) { 250 | return Peer(PeerType.GROUP, id()) 251 | } else { 252 | throw RuntimeException("Unknown type") 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /actor-bots/src/main/java/im/actor/bots/framework/MagicBotFarm.kt: -------------------------------------------------------------------------------- 1 | package im.actor.bots.framework 2 | 3 | import akka.actor.ActorSystem 4 | import akka.actor.Props 5 | import im.actor.botkit.RemoteBot 6 | import java.util.* 7 | import kotlin.reflect.KClass 8 | 9 | class BotFarm(val name: String) { 10 | 11 | var endpoint = RemoteBot.DefaultEndpoint() 12 | val system = ActorSystem.create(name) 13 | val bots = ArrayList() 14 | 15 | init { 16 | 17 | } 18 | 19 | fun bot(clazz: KClass, init: BotDescription.() -> Unit) { 20 | val b = BotDescription(clazz as KClass) 21 | b.init() 22 | bots.add(b) 23 | } 24 | 25 | fun startFarm() { 26 | 27 | for (b in bots) { 28 | var config = MagicBotConfig(b.name!!, b.clazz.java, b.overlordClazz, b.token!!, 29 | endpoint, b.traceHook) 30 | system.actorOf(Props.create(MagicalRemoteBot::class.java, config), b.name) 31 | } 32 | 33 | system.awaitTermination() 34 | } 35 | } 36 | 37 | class BotDescription(val clazz: KClass) { 38 | var name: String? = null 39 | var token: String? = null 40 | var traceHook: String? = null 41 | var overlordClazz: Class<*>? = null 42 | } 43 | 44 | public fun farm(name: String, init: BotFarm.() -> Unit) { 45 | val res = BotFarm(name) 46 | res.init() 47 | res.startFarm() 48 | } -------------------------------------------------------------------------------- /actor-bots/src/main/java/im/actor/bots/framework/i18n/I18NEngine.java: -------------------------------------------------------------------------------- 1 | package im.actor.bots.framework.i18n; 2 | 3 | import java.io.IOException; 4 | import java.util.ArrayList; 5 | import java.util.HashMap; 6 | import java.util.Properties; 7 | import java.util.Random; 8 | 9 | public class I18NEngine { 10 | 11 | private final Random random = new Random(); 12 | private HashMap> strings = new HashMap<>(); 13 | 14 | public I18NEngine(String fileName) throws IOException { 15 | 16 | Properties properties = new Properties(); 17 | properties.load(getClass().getClassLoader().getResourceAsStream(fileName)); 18 | 19 | for (String key : properties.stringPropertyNames()) { 20 | String value = new String(properties.getProperty(key).getBytes("ISO-8859-1"), "UTF-8"); 21 | 22 | String[] keyParts = key.split("\\."); 23 | try { 24 | Integer.parseInt(keyParts[keyParts.length - 1]); 25 | key = ""; 26 | for (int i = 0; i < keyParts.length - 1; i++) { 27 | if (key.length() > 0) { 28 | key += "."; 29 | } 30 | key += keyParts[i]; 31 | } 32 | } catch (Exception e) { 33 | // Expected 34 | } 35 | 36 | if (strings.containsKey(key)) { 37 | strings.get(key).add(value); 38 | } else { 39 | ArrayList s = new ArrayList<>(); 40 | s.add(value); 41 | strings.put(key, s); 42 | } 43 | } 44 | } 45 | 46 | public String pick(String key) { 47 | ArrayList s = strings.get(key); 48 | int index; 49 | synchronized (random) { 50 | index = random.nextInt(s.size()); 51 | } 52 | return s.get(index); 53 | } 54 | } -------------------------------------------------------------------------------- /actor-bots/src/main/java/im/actor/bots/framework/i18n/Strings.java: -------------------------------------------------------------------------------- 1 | package im.actor.bots.framework.i18n; 2 | 3 | import java.util.Random; 4 | 5 | public class Strings { 6 | public static final String[] UNKNOWN_MESSAGES = { 7 | "Command is invalid. Say what?", 8 | "Command is invalid. I really didn't get it...", 9 | "Command is invalid. What do you mean?", 10 | "Command is invalid. Please, say it again in a good way.", 11 | }; 12 | private static Random random = new Random(); 13 | 14 | public static String unknown() { 15 | return UNKNOWN_MESSAGES[random.nextInt(UNKNOWN_MESSAGES.length)]; 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /actor-bots/src/main/java/im/actor/bots/framework/parser/MessageCommand.java: -------------------------------------------------------------------------------- 1 | package im.actor.bots.framework.parser; 2 | 3 | import java.util.List; 4 | 5 | public class MessageCommand extends ParsedMessage { 6 | 7 | private String command; 8 | private List args; 9 | private String data; 10 | 11 | public MessageCommand(String command, List args, String data) { 12 | this.command = command; 13 | this.data = data; 14 | this.args = args; 15 | } 16 | 17 | public String getCommand() { 18 | return command; 19 | } 20 | 21 | public String getData() { 22 | return data; 23 | } 24 | 25 | public List getArgs() { 26 | return args; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /actor-bots/src/main/java/im/actor/bots/framework/parser/MessageText.java: -------------------------------------------------------------------------------- 1 | package im.actor.bots.framework.parser; 2 | 3 | public class MessageText extends ParsedMessage { 4 | 5 | final private String text; 6 | 7 | public MessageText(String text) { 8 | this.text = text; 9 | } 10 | 11 | public String getText() { 12 | return text; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /actor-bots/src/main/java/im/actor/bots/framework/parser/ParsedMessage.java: -------------------------------------------------------------------------------- 1 | package im.actor.bots.framework.parser; 2 | 3 | import java.util.ArrayList; 4 | 5 | public abstract class ParsedMessage { 6 | public static ParsedMessage matchType(String message) { 7 | message = message.trim(); 8 | if (message.startsWith("/")) { 9 | String[] data = ParsingUtils.splitFirstWord(message); 10 | String command = data[0].substring(1); 11 | ArrayList args = new ArrayList(); 12 | String text = ""; 13 | if (data.length == 2) { 14 | text = data[1]; 15 | } 16 | 17 | try { 18 | if (command.contains("(") || command.endsWith(")")) { 19 | String container = command.substring(command.indexOf('(') + 1, command.length() - 1); 20 | if (container.contains("(") || container.contains(")")) { 21 | throw new RuntimeException(); 22 | } 23 | for (String s : container.split(",")) { 24 | args.add(s.trim()); 25 | } 26 | command = command.substring(0, command.indexOf('(')); 27 | } 28 | } catch (Exception e) { 29 | e.printStackTrace(); 30 | } 31 | 32 | 33 | return new MessageCommand(command, args, text); 34 | } else { 35 | return new MessageText(message); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /actor-bots/src/main/java/im/actor/bots/framework/parser/ParsingUtils.java: -------------------------------------------------------------------------------- 1 | package im.actor.bots.framework.parser; 2 | 3 | public class ParsingUtils { 4 | 5 | public static String[] splitFirstWord(String text) { 6 | return text.trim().split(" ", 2); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /actor-bots/src/main/java/im/actor/bots/framework/persistence/KotlinExtensions.kt: -------------------------------------------------------------------------------- 1 | package im.actor.bots.framework.persistence 2 | 3 | import akka.util.Timeout 4 | import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper 5 | import com.fasterxml.jackson.module.kotlin.readValue 6 | import shardakka.keyvalue.SimpleKeyValueJava 7 | import java.io.ByteArrayOutputStream 8 | import java.util.concurrent.TimeUnit 9 | 10 | fun ServerKeyValue.setDataClass(key: String, obj: T?) { 11 | if (obj == null) { 12 | setStringValue(key, null) 13 | } else { 14 | val output = ByteArrayOutputStream() 15 | val generator = jacksonObjectMapper().jsonFactory.createGenerator(output) 16 | generator.writeObject(obj) 17 | val str = String(output.toByteArray()) 18 | setStringValue(key, str) 19 | } 20 | } 21 | 22 | inline fun ServerKeyValue.getDataClass(key: String): T? { 23 | val str = getStringValue(key) 24 | if (str == null) { 25 | return null 26 | } else { 27 | return jacksonObjectMapper().readValue(str) 28 | } 29 | } 30 | 31 | fun SimpleKeyValueJava.get(key: String): T? { 32 | val res = syncGet(key, Timeout.apply(10, TimeUnit.SECONDS)) 33 | if (res.isPresent) { 34 | return res.get() 35 | } else { 36 | return null 37 | } 38 | } -------------------------------------------------------------------------------- /actor-bots/src/main/java/im/actor/bots/framework/persistence/MagicBotPersistence.kt: -------------------------------------------------------------------------------- 1 | package im.actor.bots.framework.persistence 2 | 3 | import im.actor.bots.framework.MagicBotFork 4 | import im.actor.bots.framework.MagicForkScope 5 | import org.json.JSONObject 6 | 7 | abstract class MagicPersistentBot(scope: MagicForkScope) : MagicBotFork(scope) { 8 | 9 | // private val stateKeyValue: SimpleKeyValueJava = 10 | // ShardakkaExtension.get(context().system()).simpleKeyValue("\$${scope.name}_state_" + scope.peer.toUniqueId()).asJava() 11 | 12 | override fun preStart() { 13 | super.preStart() 14 | 15 | // val res = stateKeyValue.get("actor_state") 16 | // if (res != null) { 17 | // val state = JSONObject(res) 18 | // val internalState = state.getJSONObject("state") 19 | // onRestoreState(internalState) 20 | // } 21 | } 22 | 23 | open fun onRestoreState(state: JSONObject) { 24 | 25 | } 26 | 27 | open fun onSaveState(state: JSONObject) { 28 | 29 | } 30 | 31 | override fun afterMessage() { 32 | // saveState() 33 | } 34 | 35 | fun saveState() { 36 | // val res = JSONObject() 37 | // val internalState = JSONObject() 38 | // onSaveState(internalState) 39 | // res.put("state", internalState) 40 | // stateKeyValue.syncUpsert("actor_state", res.toString()) 41 | } 42 | } -------------------------------------------------------------------------------- /actor-bots/src/main/java/im/actor/bots/framework/persistence/ServerKeyValue.java: -------------------------------------------------------------------------------- 1 | package im.actor.bots.framework.persistence; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.jetbrains.annotations.Nullable; 5 | import org.json.JSONObject; 6 | 7 | import java.util.HashMap; 8 | import java.util.concurrent.TimeUnit; 9 | 10 | import im.actor.botkit.RemoteBot; 11 | import im.actor.bots.BotMessages; 12 | import scala.Option; 13 | import scala.concurrent.Await; 14 | import scala.concurrent.duration.Duration; 15 | 16 | import static scala.compat.java8.JFunction.proc; 17 | 18 | public class ServerKeyValue { 19 | 20 | private static final String KEY_SPACE = "default"; 21 | 22 | private String keySpace; 23 | private RemoteBot remoteBot; 24 | private HashMap cachedValues = new HashMap(); 25 | 26 | public ServerKeyValue(@NotNull RemoteBot remoteBot) { 27 | this(remoteBot, KEY_SPACE); 28 | } 29 | 30 | public ServerKeyValue(@NotNull RemoteBot remoteBot, @NotNull String keySpace) { 31 | this.keySpace = keySpace; 32 | this.remoteBot = remoteBot; 33 | } 34 | 35 | public void setStringValue(@NotNull String key, @Nullable String value) { 36 | cachedValues.put(key, value); 37 | remoteBot.requestSetValue(keySpace, key, value).foreach(proc(s -> { 38 | 39 | }), remoteBot.context().dispatcher()); 40 | } 41 | 42 | @Nullable 43 | public String getStringValue(@NotNull String key) throws Exception { 44 | if (cachedValues.containsKey(key)) { 45 | return cachedValues.get(key); 46 | } 47 | BotMessages.Container> res = Await.result(remoteBot.requestGetValue(keySpace, key), Duration.create(60, TimeUnit.SECONDS)); 48 | if (res.value().nonEmpty()) { 49 | String val = res.value().get(); 50 | cachedValues.put(key, val); 51 | return val; 52 | } 53 | return null; 54 | } 55 | 56 | public void setDoubleValue(@NotNull String key, @Nullable Double value) { 57 | if (value != null) { 58 | setStringValue(key, Double.toString(value)); 59 | } else { 60 | setStringValue(key, null); 61 | } 62 | } 63 | 64 | @Nullable 65 | public Double getDoubleValue(@NotNull String key) throws Exception { 66 | String res = getStringValue(key); 67 | if (res != null) { 68 | return Double.parseDouble(res); 69 | } else { 70 | return null; 71 | } 72 | } 73 | 74 | public void setIntValue(@NotNull String key, @Nullable Integer value) { 75 | if (value != null) { 76 | setStringValue(key, Integer.toString(value)); 77 | } else { 78 | setStringValue(key, null); 79 | } 80 | } 81 | 82 | @Nullable 83 | public Integer getIntValue(@NotNull String key) throws Exception { 84 | String res = getStringValue(key); 85 | if (res != null) { 86 | return Integer.parseInt(res); 87 | } else { 88 | return null; 89 | } 90 | } 91 | 92 | public void setBoolValue(@NotNull String key, @Nullable Boolean value) { 93 | if (value != null) { 94 | setStringValue(key, Boolean.toString(value)); 95 | } else { 96 | setStringValue(key, null); 97 | } 98 | } 99 | 100 | @Nullable 101 | public Boolean getBoolValue(@NotNull String key) throws Exception { 102 | String res = getStringValue(key); 103 | if (res != null) { 104 | return Boolean.parseBoolean(res); 105 | } else { 106 | return null; 107 | } 108 | } 109 | 110 | public boolean getBoolValue(@NotNull String key, boolean value) throws Exception { 111 | String res = getStringValue(key); 112 | if (res != null) { 113 | return Boolean.parseBoolean(res); 114 | } else { 115 | return value; 116 | } 117 | } 118 | 119 | public void setLongValue(@NotNull String key, @Nullable Long value) { 120 | if (value != null) { 121 | setStringValue(key, Long.toString(value)); 122 | } else { 123 | setStringValue(key, null); 124 | } 125 | } 126 | 127 | @Nullable 128 | public Long getLongValue(@NotNull String key) throws Exception { 129 | String res = getStringValue(key); 130 | if (res != null) { 131 | return Long.parseLong(res); 132 | } else { 133 | return null; 134 | } 135 | } 136 | 137 | public void setJSONValue(@NotNull String key, @Nullable JSONObject value) { 138 | if (value != null) { 139 | setStringValue(key, value.toString()); 140 | } else { 141 | setStringValue(key, null); 142 | } 143 | } 144 | 145 | @Nullable 146 | public JSONObject getJSONValue(@NotNull String key) throws Exception { 147 | String res = getStringValue(key); 148 | if (res != null) { 149 | return new JSONObject(res); 150 | } else { 151 | return null; 152 | } 153 | } 154 | } -------------------------------------------------------------------------------- /actor-bots/src/main/java/im/actor/bots/framework/stateful/Expect.kt: -------------------------------------------------------------------------------- 1 | package im.actor.bots.framework.stateful 2 | 3 | import im.actor.bots.BotMessages 4 | import im.actor.bots.framework.* 5 | import im.actor.bots.framework.traits.ModernMessage 6 | import org.json.JSONObject 7 | import java.util.* 8 | 9 | abstract class Expect(val stateName: String, val parent: Expect?) : ExpectContainer { 10 | 11 | var defaultState: String? = null 12 | var child = HashMap() 13 | 14 | private var beforeClosure: (ExpectContext.() -> Unit)? = null 15 | 16 | fun before(before: (ExpectContext.() -> Unit)?) { 17 | beforeClosure = before 18 | } 19 | 20 | open fun onReceived(context: ExpectContext) { 21 | 22 | } 23 | 24 | open fun onBefore(context: ExpectContext) { 25 | if (beforeClosure != null) { 26 | applyContext(context, beforeClosure!!) 27 | } 28 | } 29 | 30 | override fun addChild(expect: Expect) { 31 | if (defaultState == null) { 32 | defaultState = expect.stateName 33 | } 34 | child.put(expect.stateName, expect) 35 | } 36 | 37 | protected fun applyContext(context: ExpectContext, closure: ExpectContext.() -> Unit) { 38 | context.closure() 39 | } 40 | 41 | protected fun applyContext(context: ExpectContext, closure: ExpectContext.() -> Boolean): Boolean { 42 | return context.closure() 43 | } 44 | 45 | fun fullName(): String { 46 | var res = stateName 47 | if (parent != null) { 48 | res = parent.fullName() + "." + res 49 | } 50 | return res 51 | } 52 | 53 | override fun getContainer(): Expect? { 54 | return this 55 | } 56 | } 57 | 58 | interface ExpectContext { 59 | var body: MagicBotMessage? 60 | get 61 | 62 | fun goto(stateId: String) 63 | fun tryGoto(stateId: String): Boolean 64 | fun gotoParent(level: Int) 65 | fun gotoParent() 66 | fun log(text: String) 67 | fun sendText(text: String) 68 | fun sendJson(dataType: String, json: JSONObject) 69 | fun sendModernText(message: ModernMessage) 70 | } 71 | 72 | interface ExpectContainer { 73 | fun addChild(expect: Expect) 74 | fun getContainer(): Expect? 75 | } 76 | 77 | interface ExpectCommandContainer : ExpectContainer { 78 | 79 | } 80 | 81 | var ExpectContext.text: String 82 | get() { 83 | if (body is MagicBotTextMessage) { 84 | return (body as MagicBotTextMessage).text!! 85 | } 86 | throw RuntimeException() 87 | } 88 | private set(v) { 89 | 90 | } 91 | 92 | var ExpectContext.isText: Boolean 93 | get() { 94 | if (body is MagicBotTextMessage) { 95 | return (body as MagicBotTextMessage).command == null 96 | } 97 | return false 98 | } 99 | private set(v) { 100 | 101 | } 102 | 103 | var ExpectContext.isCommand: Boolean 104 | get() { 105 | if (body is MagicBotTextMessage) { 106 | return (body as MagicBotTextMessage).command != null 107 | } 108 | return false 109 | } 110 | private set(v) { 111 | 112 | } 113 | 114 | var ExpectContext.command: String? 115 | get() { 116 | if (body is MagicBotTextMessage) { 117 | return (body as MagicBotTextMessage).command 118 | } 119 | return null 120 | } 121 | private set(v) { 122 | 123 | } 124 | 125 | var ExpectContext.commandArgs: String? 126 | get() { 127 | if (body is MagicBotTextMessage) { 128 | return (body as MagicBotTextMessage).commandArgs 129 | } 130 | return null 131 | } 132 | private set(v) { 133 | 134 | } 135 | 136 | var ExpectContext.isCancel: Boolean 137 | get() { 138 | return isCommand && command == "cancel" 139 | } 140 | private set(v) { 141 | 142 | } 143 | 144 | var ExpectContext.isDoc: Boolean 145 | get() { 146 | return body is MagicBotDocMessage 147 | } 148 | private set(v) { 149 | 150 | } 151 | 152 | var ExpectContext.doc: BotMessages.DocumentMessage 153 | get() { 154 | if (body is MagicBotDocMessage) { 155 | return (body as MagicBotDocMessage).doc 156 | } 157 | throw RuntimeException() 158 | } 159 | private set(v) { 160 | 161 | } 162 | 163 | 164 | var ExpectContext.isSticker: Boolean 165 | get() { 166 | return body is MagicBotStickerMessage 167 | } 168 | private set(v) { 169 | 170 | } 171 | 172 | var ExpectContext.sticker: BotMessages.StickerMessage 173 | get() { 174 | if (body is MagicBotStickerMessage) { 175 | return (body as MagicBotStickerMessage).sticker 176 | } 177 | throw RuntimeException() 178 | } 179 | private set(v) { 180 | 181 | } 182 | 183 | 184 | var ExpectContext.isPhoto: Boolean 185 | get() { 186 | if (body is MagicBotDocMessage) { 187 | val doc = body as MagicBotDocMessage 188 | if (doc.doc.ext.isPresent && doc.doc.ext.get() is BotMessages.DocumentExPhoto) { 189 | return true 190 | } 191 | } 192 | return false 193 | } 194 | private set(v) { 195 | 196 | } 197 | 198 | var ExpectContext.responseJson: JSONObject 199 | get() { 200 | if (body is MagicBotJsonMessage) { 201 | return (body as MagicBotJsonMessage).json 202 | } 203 | throw RuntimeException() 204 | } 205 | private set(v) { 206 | 207 | } 208 | 209 | var ExpectContext.isJson: Boolean 210 | get() { 211 | return body is MagicBotJsonMessage 212 | } 213 | private set(v) { 214 | 215 | } -------------------------------------------------------------------------------- /actor-bots/src/main/java/im/actor/bots/framework/stateful/ExpectCommands.kt: -------------------------------------------------------------------------------- 1 | package im.actor.bots.framework.stateful 2 | 3 | import im.actor.bots.framework.MagicBotTextMessage 4 | 5 | class ExpectCommands(name: String, parent: Expect?) : Expect(name, parent), ExpectCommandContainer { 6 | 7 | private var unknownCommand: (ExpectContext.() -> Unit)? = null 8 | private var notACommand: (ExpectContext.() -> Unit)? = null 9 | 10 | override fun onReceived(context: ExpectContext) { 11 | 12 | if (context.body is MagicBotTextMessage) { 13 | val textMessage = context.body as MagicBotTextMessage 14 | if (textMessage.command != null) { 15 | if (child.containsKey("/" + textMessage.command!!)) { 16 | 17 | context.goto("/" + textMessage.command!!) 18 | return 19 | } else { 20 | 21 | // Unknown command 22 | if (unknownCommand != null) { 23 | applyContext(context, unknownCommand!!) 24 | } else { 25 | context.tryGoto("default") 26 | } 27 | 28 | return 29 | } 30 | } 31 | } 32 | 33 | if (notACommand != null) { 34 | applyContext(context, notACommand!!) 35 | } else { 36 | context.tryGoto("default") 37 | } 38 | } 39 | 40 | override fun getContainer(): ExpectCommands { 41 | return this 42 | } 43 | } 44 | 45 | class ExpectCommand(name: String, parent: Expect?) : Expect(name, parent) { 46 | 47 | 48 | override fun onReceived(context: ExpectContext) { 49 | 50 | } 51 | } 52 | 53 | public fun ExpectCommandContainer.command(name: String, init: (ExpectCommand.() -> Unit)): ExpectCommand { 54 | val res = ExpectCommand(name, getContainer()) 55 | addChild(res) 56 | res.init() 57 | return res 58 | } 59 | 60 | public fun ExpectContainer.oneShot(name: String, init: (ExpectContext.() -> Unit)): ExpectCommand { 61 | val res = ExpectCommand(name, getContainer()) 62 | res.before { 63 | init() 64 | gotoParent() 65 | } 66 | addChild(res) 67 | return res 68 | } 69 | 70 | public fun ExpectCommandContainer.expectCommands(init: (ExpectCommands.() -> Unit)): ExpectCommands { 71 | return expectCommands("main", init) 72 | } 73 | 74 | public fun ExpectCommandContainer.expectCommands(name: String, init: (ExpectCommands.() -> Unit)): ExpectCommands { 75 | val res = ExpectCommands(name, getContainer()) 76 | addChild(res) 77 | res.init() 78 | return res 79 | } -------------------------------------------------------------------------------- /actor-bots/src/main/java/im/actor/bots/framework/stateful/ExpectInput.kt: -------------------------------------------------------------------------------- 1 | package im.actor.bots.framework.stateful 2 | 3 | open class ExpectInput(stateName: String, parent: Expect?) : Expect(stateName, parent) { 4 | 5 | protected var receivedClosure: (ExpectContext.() -> Unit)? = null 6 | 7 | fun received(receive: (ExpectContext.() -> Unit)?) { 8 | receivedClosure = receive 9 | } 10 | 11 | override fun onReceived(context: ExpectContext) { 12 | if (receivedClosure != null) { 13 | applyContext(context, receivedClosure!!) 14 | } 15 | } 16 | } 17 | 18 | open class ExpectValidatedInput(stateName: String, parent: Expect?) : ExpectInput(stateName, parent) { 19 | 20 | private var validateClosure: (ExpectContext.() -> Boolean)? = null 21 | 22 | fun validate(validate: (ExpectContext.() -> Boolean)?) { 23 | validateClosure = validate 24 | } 25 | 26 | override fun onReceived(context: ExpectContext) { 27 | if (applyContext(context, validateClosure!!)) { 28 | if (receivedClosure != null) { 29 | applyContext(context, receivedClosure!!) 30 | } 31 | } 32 | } 33 | } 34 | 35 | fun ExpectContainer.expectInput(name: String, init: ((ExpectValidatedInput.() -> Unit))): ExpectValidatedInput { 36 | val res = ExpectValidatedInput(name, getContainer()) 37 | addChild(res) 38 | res.init() 39 | return res 40 | } -------------------------------------------------------------------------------- /actor-bots/src/main/java/im/actor/bots/framework/stateful/ExpectRaw.kt: -------------------------------------------------------------------------------- 1 | package im.actor.bots.framework.stateful 2 | 3 | class ExpectRaw(stateName: String, parent: Expect?) : Expect(stateName, parent) { 4 | 5 | private var receiveClosure: (ExpectContext.() -> Unit)? = null 6 | 7 | fun received(receive: (ExpectContext.() -> Unit)?) { 8 | receiveClosure = receive 9 | } 10 | 11 | override fun onReceived(context: ExpectContext) { 12 | if (receiveClosure != null) { 13 | applyContext(context, receiveClosure!!) 14 | } 15 | } 16 | } 17 | 18 | fun ExpectContainer.raw(name: String, init: ((ExpectRaw.() -> Unit))): ExpectRaw { 19 | val res = ExpectRaw(name, getContainer()) 20 | addChild(res) 21 | res.init() 22 | return res 23 | } 24 | 25 | fun ExpectContainer.static(name: String, init: ((ExpectContext.() -> Unit))): ExpectRaw { 26 | val res = ExpectRaw(name, getContainer()) 27 | addChild(res) 28 | res.before { 29 | init() 30 | } 31 | return res 32 | } -------------------------------------------------------------------------------- /actor-bots/src/main/java/im/actor/bots/framework/stateful/MagicBotStateful.kt: -------------------------------------------------------------------------------- 1 | package im.actor.bots.framework.stateful 2 | 3 | import im.actor.bots.framework.MagicBotMessage 4 | import im.actor.bots.framework.MagicForkScope 5 | import im.actor.bots.framework.i18n.Strings 6 | import im.actor.bots.framework.persistence.MagicPersistentBot 7 | import org.json.JSONObject 8 | 9 | abstract class MagicStatefulBot(scope: MagicForkScope) : MagicPersistentBot(scope), ExpectContext, ExpectCommandContainer { 10 | 11 | var enablePersistent = false 12 | var currentBody: MagicBotMessage? = null 13 | var root: Expect? = null 14 | var currentState: Expect? = null 15 | 16 | override fun preStart() { 17 | configure() 18 | if (root == null) { 19 | throw RuntimeException("Root state not installed") 20 | } 21 | // Initial state will set in onRestoreState 22 | super.preStart() 23 | if (currentState == null) { 24 | currentState = root 25 | } 26 | } 27 | 28 | override fun onSaveState(state: JSONObject) { 29 | if (enablePersistent) { 30 | state.put("#state_id", currentState!!.fullName()) 31 | } 32 | } 33 | 34 | override fun onRestoreState(state: JSONObject) { 35 | if (enablePersistent) { 36 | val id = state.optString("#state_id", root!!.fullName()) 37 | log("Loaded name $id") 38 | val dest = findExpect(id, root!!) 39 | if (dest != null) { 40 | log("Found dest ${dest.fullName()}") 41 | currentState = dest 42 | } else { 43 | log("Unable to found") 44 | currentState = root 45 | } 46 | } else { 47 | currentState = root 48 | } 49 | 50 | } 51 | 52 | abstract fun configure() 53 | 54 | /** 55 | * Enabling Simple Mode. Useful for simple geeky bots that works only in private chats 56 | * and consist of just list of commands. 57 | */ 58 | fun enableSimpleMode(hint: String, unknown: String? = null) { 59 | 60 | enableInGroups = false 61 | enablePersistent = true 62 | 63 | // 64 | // Enable error on unknown command 65 | // 66 | 67 | oneShot("default") { 68 | if (unknown != null) { 69 | sendText(localized(unknown)) 70 | } else { 71 | sendText(Strings.unknown()) 72 | } 73 | } 74 | 75 | // 76 | // Hint 77 | // 78 | 79 | oneShot("/start") { 80 | sendText(hint) 81 | } 82 | } 83 | 84 | override fun onMessage(message: MagicBotMessage) { 85 | currentBody = message 86 | currentState!!.onReceived(this) 87 | } 88 | 89 | // Context 90 | 91 | override var body: MagicBotMessage? 92 | get() { 93 | return currentBody 94 | } 95 | set(value) { 96 | } 97 | 98 | override fun goto(stateId: String) { 99 | val n = findExpect(stateId, currentState!!) ?: throw RuntimeException("Unable to find $stateId") 100 | currentState = n 101 | log("goto: ${currentState!!.fullName()}") 102 | n.onBefore(this) 103 | } 104 | 105 | override fun tryGoto(stateId: String): Boolean { 106 | val n = findExpect(stateId, currentState!!) 107 | if (n != null) { 108 | currentState = n 109 | log("tryGoto: ${currentState!!.fullName()}") 110 | n.onBefore(this) 111 | return true 112 | } else { 113 | return false 114 | } 115 | } 116 | 117 | private fun findExpect(stateId: String, start: Expect): Expect? { 118 | var parts = stateId.split(".") 119 | // Finding with starting included 120 | return findExpectUp(parts, start) 121 | // if (res != null) { 122 | // return res 123 | // } 124 | //// // Finding in all children 125 | //// for (c in start.child.values) { 126 | //// val res = findExpectFrom(parts, c) 127 | //// if (res != null) { 128 | //// return res 129 | //// } 130 | //// } 131 | //// 132 | //// // Finding in root 133 | //// val resRoot = findExpectFrom(parts, root!!) 134 | //// if (resRoot != null) { 135 | //// return resRoot 136 | //// } 137 | //// 138 | //// // Finding in root children 139 | //// for (c in root!!.child.values) { 140 | //// val res = findExpectFrom(parts, c) 141 | //// if (res != null) { 142 | //// return res 143 | //// } 144 | //// } 145 | // 146 | // // Nothing found 147 | // return null 148 | } 149 | 150 | private fun findExpectUp(stateIds: List, start: Expect): Expect? { 151 | val res = findExpectFrom(stateIds, start) 152 | if (res != null) { 153 | return res 154 | } 155 | if (start.parent != null) { 156 | return findExpectUp(stateIds, start.parent) 157 | } else { 158 | return null 159 | } 160 | } 161 | 162 | private fun findExpectFrom(stateIds: List, start: Expect): Expect? { 163 | if (stateIds.count() == 0) { 164 | return start 165 | } 166 | val id = stateIds[0] 167 | if (start.stateName == id) { 168 | if (stateIds.count() == 1) { 169 | return start 170 | } 171 | for (i in start.child.values) { 172 | val res = findExpectFrom(stateIds.drop(1).toList(), i) 173 | if (res != null) { 174 | return res 175 | } 176 | } 177 | } 178 | for (i in start.child.values) { 179 | if (i.stateName == id) { 180 | return findExpectFrom(stateIds.drop(1).toList(), i) 181 | } 182 | } 183 | // if (start.stateName == id) { 184 | // if (stateIds.count() == 1) { 185 | // return start 186 | // } 187 | // for (i in start.child.values) { 188 | // val res = findExpectFrom(stateIds.drop(1).toList(), i) 189 | // if (res != null) { 190 | // return res 191 | // } 192 | // } 193 | // } 194 | 195 | return null 196 | } 197 | 198 | override fun gotoParent() { 199 | if (currentState != null && currentState!!.parent != null) { 200 | currentState = currentState!!.parent!! 201 | log("GotoParent: ${currentState!!.fullName()}") 202 | currentState!!.onBefore(this) 203 | } else { 204 | throw RuntimeException("${currentState!!.fullName()} doesn't have parent") 205 | } 206 | } 207 | 208 | override fun gotoParent(level: Int) { 209 | 210 | } 211 | 212 | override fun log(text: String) { 213 | v(text) 214 | } 215 | 216 | // Container 217 | 218 | override fun addChild(expect: Expect) { 219 | getContainer().addChild(expect) 220 | } 221 | 222 | override fun getContainer(): ExpectCommands { 223 | if (root != null) { 224 | if (root !is ExpectCommands) { 225 | throw RuntimeException("Root is not commands container") 226 | } 227 | } else { 228 | root = ExpectCommands("main", null) 229 | } 230 | return root as ExpectCommands 231 | } 232 | 233 | protected fun formSend(name: String, toSend: String): String { 234 | return "[$name](send:$toSend)" 235 | } 236 | 237 | protected fun formSend(command: String): String { 238 | return formSend(command, command) 239 | } 240 | } -------------------------------------------------------------------------------- /actor-bots/src/main/java/im/actor/bots/framework/traits/APITrait.kt: -------------------------------------------------------------------------------- 1 | package im.actor.bots.framework.traits 2 | 3 | import im.actor.botkit.RemoteBot 4 | import im.actor.bots.BotMessages 5 | import im.actor.bots.framework.OutPeer 6 | import org.json.JSONObject 7 | import scala.Option 8 | import scala.concurrent.Await 9 | import scala.concurrent.duration.Duration 10 | import java.util.* 11 | import java.util.concurrent.TimeUnit 12 | 13 | interface APITrait { 14 | 15 | // 16 | // Messaging 17 | // 18 | 19 | fun sendText(peer: OutPeer, text: String) 20 | 21 | fun sendJson(peer: OutPeer, dataType: String, json: JSONObject) 22 | 23 | fun sendModernText(peer: OutPeer, message: ModernMessage) 24 | 25 | fun findUser(query: String): BotMessages.User? 26 | 27 | fun getUser(uid: Int): BotMessages.User 28 | 29 | fun getGroup(gid: Int): BotMessages.Group 30 | 31 | fun createGroup(groupTitle: String): BotMessages.ResponseCreateGroup? 32 | 33 | fun inviteUserToGroup(group: BotMessages.GroupOutPeer, user: BotMessages.UserOutPeer): Boolean 34 | 35 | // 36 | // Managing Hooks 37 | // 38 | 39 | fun createHook(hookName: String): String? 40 | 41 | // 42 | // Super Bot methods 43 | // 44 | 45 | fun createBot(userName: String, name: String): BotMessages.BotCreated? 46 | fun changeUserName(uid: Int, name: String): Boolean 47 | fun changeUserAvatar(uid: Int, fileId: Long, accessHash: Long): Boolean 48 | fun changeUserAbout(uid: Int, name: String): Boolean 49 | fun userIsAdmin(uid: Int): Boolean 50 | 51 | fun createStickerPack(ownerUserId: Int): Int 52 | fun addSticker(ownerUserId: Int, packId: Int, emoji: Optional, image128: ByteArray, w128: Int, h128: Int, image256: ByteArray, w256: Int, h256: Int, image512: ByteArray, w512: Int, h512: Int): Boolean 53 | fun showStickerPacks(ownerUserId: Int): List 54 | fun showStickers(ownerUserId: Int, packId: Int): List 55 | fun deleteSticker(ownerUserId: Int, packId: Int, stickerId: Int): Boolean 56 | fun makeStickerPackDefault(userId: Int, packId: Int): Boolean 57 | fun unmakeStickerPackDefault(userId: Int, packId: Int): Boolean 58 | 59 | fun getFile(fileId: Long, accessHash: Long): ByteArray 60 | } 61 | 62 | 63 | class ModernMessage(val compatText: String) { 64 | var text: String? = null 65 | var paragraphStyle: ParagraphStyle? = null 66 | var attaches = ArrayList() 67 | } 68 | 69 | class ParagraphStyle { 70 | var showParagraph: Boolean = false 71 | var paragraphColor: Color? = null 72 | var backgroundColor: Color? = null 73 | } 74 | 75 | class ModernAttach { 76 | var text: String? = null 77 | var title: String? = null 78 | var titleUrl: String? = null 79 | var paragraphStyle: ParagraphStyle? = null 80 | var fields = ArrayList() 81 | } 82 | 83 | class ModernField(val name: String, val value: String, val isShort: Boolean) { 84 | } 85 | 86 | sealed class Color { 87 | class RGB(val number: Int) : Color() 88 | 89 | object Red : Color() 90 | 91 | object Green : Color() 92 | 93 | object Yellow : Color() 94 | } 95 | 96 | interface APITraitScoped : APITrait { 97 | fun sendText(text: String) 98 | fun sendJson(dataType: String, json: JSONObject) 99 | fun sendModernText(message: ModernMessage) 100 | } 101 | 102 | open class APITraitImpl(val bot: RemoteBot) : APITrait { 103 | 104 | override fun inviteUserToGroup(group: BotMessages.GroupOutPeer, user: BotMessages.UserOutPeer): Boolean { 105 | try { 106 | Await.result(bot.requestInviteUser(group, user), Duration.create(50, TimeUnit.SECONDS)) 107 | return true 108 | } catch(e: Exception) { 109 | return false 110 | } 111 | } 112 | 113 | override fun sendText(peer: OutPeer, text: String) { 114 | bot.requestSendMessage(peer.toKit(), bot.nextRandomId(), BotMessages.TextMessage(text, Option.empty())) 115 | } 116 | 117 | override fun sendJson(peer: OutPeer, dataType: String, data: JSONObject) { 118 | val jsonMsg = JSONObject() 119 | jsonMsg.put("dataType", dataType); 120 | jsonMsg.put("data", data) 121 | bot.requestSendMessage(peer.toKit(), bot.nextRandomId(), BotMessages.JsonMessage(jsonMsg.toString())) 122 | } 123 | 124 | override fun sendModernText(peer: OutPeer, message: ModernMessage) { 125 | val paragraphStyle = convertParagraphStyle(message.paragraphStyle) 126 | var attachesList = ArrayList() 127 | for (m in message.attaches) { 128 | var attrs = ArrayList() 129 | for (f in m.fields) { 130 | attrs.add(BotMessages.TextModernField(f.name, 131 | f.value, Option.apply(f.isShort))) 132 | } 133 | attachesList.add(BotMessages.TextModernAttach(m.title, 134 | m.titleUrl, null, m.text, convertParagraphStyle(m.paragraphStyle), attrs)) 135 | } 136 | bot.requestSendMessage(peer.toKit(), bot.nextRandomId(), BotMessages.TextMessage(message.compatText, 137 | Option.apply(BotMessages.TextModernMessage(message.text, 138 | null, 139 | null, 140 | paragraphStyle, attachesList)))) 141 | } 142 | 143 | private fun convertParagraphStyle(style: ParagraphStyle?): BotMessages.ParagraphStyle? { 144 | if (style == null) { 145 | return null 146 | } 147 | 148 | return BotMessages.ParagraphStyle(Option.apply(style.showParagraph), 149 | convertColor(style.paragraphColor), 150 | convertColor(style.backgroundColor)) 151 | } 152 | 153 | private fun convertColor(color: Color?): Option { 154 | if (color == null) { 155 | return Option.empty() 156 | } 157 | when (color) { 158 | Color.Green -> { 159 | return Option.apply(BotMessages.PredefinedColor(BotMessages.`Green$`())) 160 | } 161 | Color.Red -> { 162 | return Option.apply(BotMessages.PredefinedColor(BotMessages.`Red$`())) 163 | } 164 | Color.Yellow -> { 165 | return Option.apply(BotMessages.PredefinedColor(BotMessages.`Yellow$`())) 166 | } 167 | is Color.RGB -> { 168 | return Option.apply(BotMessages.RgbColor(color.number)) 169 | } 170 | } 171 | } 172 | 173 | override fun findUser(query: String): BotMessages.User? { 174 | try { 175 | val res = Await.result(bot.requestFindUser(query), Duration.create(50, TimeUnit.SECONDS)) 176 | if (res.users.isEmpty()) { 177 | return null 178 | } 179 | return res.users[0] 180 | } catch(e: Exception) { 181 | return null 182 | } 183 | } 184 | 185 | override fun getUser(uid: Int): BotMessages.User { 186 | return bot.getUser(uid) 187 | } 188 | 189 | override fun getGroup(gid: Int): BotMessages.Group { 190 | return bot.getGroup(gid) 191 | } 192 | 193 | override fun createGroup(groupTitle: String): BotMessages.ResponseCreateGroup? { 194 | try { 195 | return Await.result(bot.requestCreateGroup(groupTitle), Duration.create(50, TimeUnit.SECONDS)) 196 | } catch(e: Exception) { 197 | return null 198 | } 199 | } 200 | 201 | override fun createHook(hookName: String): String? { 202 | try { 203 | return Await.result(bot.requestRegisterHook(hookName), Duration.create(50, TimeUnit.SECONDS)).value() 204 | } catch(e: Exception) { 205 | return null 206 | } 207 | } 208 | 209 | override fun createBot(userName: String, name: String): BotMessages.BotCreated? { 210 | try { 211 | return Await.result(bot.requestCreateBot(userName, name), Duration.create(50, TimeUnit.SECONDS)) 212 | } catch(e: Exception) { 213 | return null 214 | } 215 | } 216 | 217 | override fun changeUserName(uid: Int, name: String): Boolean { 218 | try { 219 | Await.result(bot.requestChangeUserName(uid, name), 220 | Duration.create(50, TimeUnit.SECONDS)) 221 | return true 222 | } catch(e: Exception) { 223 | e.printStackTrace() 224 | } 225 | return false 226 | } 227 | 228 | override fun changeUserAvatar(uid: Int, fileId: Long, accessHash: Long): Boolean { 229 | try { 230 | Await.result(bot.requestChangeUserAvatar(uid, 231 | BotMessages.FileLocation(fileId, accessHash)), 232 | Duration.create(50, TimeUnit.SECONDS)) 233 | return true 234 | } catch(e: Exception) { 235 | e.printStackTrace() 236 | } 237 | return false 238 | } 239 | 240 | override fun changeUserAbout(uid: Int, name: String): Boolean { 241 | try { 242 | Await.result(bot.requestChangeUserAbout(uid, Optional.of(name)), 243 | Duration.create(50, TimeUnit.SECONDS)) 244 | return true 245 | } catch(e: Exception) { 246 | e.printStackTrace() 247 | } 248 | return false 249 | } 250 | 251 | override fun userIsAdmin(uid: Int): Boolean { 252 | try { 253 | return Await.result(bot.requestIsAdmin(uid), Duration.create(50, TimeUnit.SECONDS)).getIsAdmin() 254 | } catch(e: Exception) { 255 | e.printStackTrace() 256 | } 257 | return false 258 | } 259 | 260 | //TODO: remove magic numbers 261 | override fun createStickerPack(ownerUserId: Int): Int { 262 | try { 263 | return Await.result(bot.requestCreateStickerPack(ownerUserId), Duration.create(50, TimeUnit.SECONDS)).value().toInt() 264 | } catch(e: Exception) { 265 | e.printStackTrace() 266 | } 267 | return -1 268 | } 269 | 270 | override fun addSticker(ownerUserId: Int, packId: Int, emoji: Optional, image128: ByteArray, w128: Int, h128: Int, image256: ByteArray, w256: Int, h256: Int, image512: ByteArray, w512: Int, h512: Int): Boolean { 271 | try { 272 | Await.result(bot.requestAddSticker(ownerUserId, packId, emoji, image128, w128, h128, image256, w256, h256, image512, w512, h512), 273 | Duration.create(50, TimeUnit.SECONDS)) 274 | return true 275 | } catch(e: Exception) { 276 | e.printStackTrace() 277 | } 278 | return false 279 | } 280 | 281 | override fun showStickerPacks(ownerUserId: Int): List { 282 | try { 283 | return Await.result(bot.requestShowStickerPacks(ownerUserId), Duration.create(50, TimeUnit.SECONDS)).getIds() 284 | } catch(e: Exception) { 285 | e.printStackTrace() 286 | } 287 | return ArrayList() 288 | } 289 | 290 | override fun showStickers(ownerUserId: Int, packId: Int): List { 291 | try { 292 | return Await.result(bot.requestShowStickers(ownerUserId, packId), Duration.create(50, TimeUnit.SECONDS)).getIds() 293 | } catch(e: Exception) { 294 | e.printStackTrace() 295 | } 296 | return ArrayList() 297 | } 298 | 299 | override fun deleteSticker(ownerUserId: Int, packId: Int, stickerId: Int): Boolean { 300 | try { 301 | Await.result(bot.requestDeleteSticker(ownerUserId, packId, stickerId), Duration.create(50, TimeUnit.SECONDS)) 302 | return true 303 | } catch(e: Exception) { 304 | e.printStackTrace() 305 | } 306 | return false 307 | } 308 | 309 | override fun makeStickerPackDefault(userId: Int, packId: Int): Boolean { 310 | try { 311 | Await.result(bot.requestMakeStickerPackDefault(userId, packId), Duration.create(50, TimeUnit.SECONDS)) 312 | return true 313 | } catch(e: Exception) { 314 | e.printStackTrace() 315 | } 316 | return false 317 | } 318 | 319 | override fun unmakeStickerPackDefault(userId: Int, packId: Int): Boolean { 320 | try { 321 | Await.result(bot.requestUnmakeStickerPackDefault(userId, packId), Duration.create(50, TimeUnit.SECONDS)) 322 | return true 323 | } catch(e: Exception) { 324 | e.printStackTrace() 325 | } 326 | return false 327 | } 328 | 329 | override fun getFile(fileId: Long, accessHash: Long): ByteArray { 330 | try { 331 | return Await.result(bot.requestDownloadFile(BotMessages.FileLocation(fileId, accessHash)), Duration.create(50, TimeUnit.SECONDS)).fileBytes() 332 | } catch(e: Exception) { 333 | e.printStackTrace() 334 | } 335 | return ByteArray(0) 336 | } 337 | 338 | } 339 | 340 | class APITraitScopedImpl(val peer: OutPeer, bot: RemoteBot) : APITraitImpl(bot), APITraitScoped { 341 | override fun sendText(text: String) { 342 | sendText(peer, text) 343 | } 344 | 345 | override fun sendJson(dataType: String, json: JSONObject) { 346 | sendJson(peer, dataType, json) 347 | } 348 | 349 | override fun sendModernText(message: ModernMessage) { 350 | sendModernText(peer, message) 351 | } 352 | } -------------------------------------------------------------------------------- /actor-bots/src/main/java/im/actor/bots/framework/traits/AdminTrait.kt: -------------------------------------------------------------------------------- 1 | package im.actor.bots.framework.traits 2 | 3 | import im.actor.bots.BotMessages 4 | import im.actor.bots.framework.MagicForkScope 5 | import java.util.* 6 | 7 | /** 8 | * Trait for enabling administration control of Bot. Enables admins nicknames and checking if user is 9 | * bots's admin 10 | */ 11 | interface AdminTrait { 12 | var admins: MutableList 13 | fun isAdmin(user: BotMessages.User?): Boolean 14 | } 15 | 16 | interface AdminTraitScoped : AdminTrait { 17 | fun isAdminScope(): Boolean 18 | } 19 | 20 | open class AdminTraitImpl : AdminTrait { 21 | 22 | override var admins: MutableList = ArrayList() 23 | 24 | override fun isAdmin(user: BotMessages.User?): Boolean { 25 | if (user == null) { 26 | return false 27 | } 28 | if (user.username.isPresent) { 29 | return admins.contains(user.username.get()) 30 | } else { 31 | return false 32 | } 33 | } 34 | } 35 | 36 | class AdminTraitScopedImpl(val scope: MagicForkScope) : AdminTraitImpl(), AdminTraitScoped { 37 | 38 | override fun isAdminScope(): Boolean { 39 | 40 | if (scope.peer.isPrivate) { 41 | try { 42 | val usr = scope.bot.getUser(scope.peer.id) ?: return false 43 | return isAdmin(usr) 44 | } catch(e: Exception) { 45 | 46 | } 47 | } 48 | 49 | return false 50 | } 51 | 52 | } -------------------------------------------------------------------------------- /actor-bots/src/main/java/im/actor/bots/framework/traits/AiTrait.kt: -------------------------------------------------------------------------------- 1 | package im.actor.bots.framework.traits 2 | 3 | import org.json.JSONObject 4 | import java.util.* 5 | 6 | /** 7 | * API.AI Integration trait. 8 | * Before use set your Subscription Key and register agents 9 | */ 10 | interface AiTrait { 11 | var aiSubscriptionKey: String? 12 | fun registerAiAgent(key: String, lang: String, token: String) 13 | 14 | fun aiQuery(query: String): AiResponse? 15 | 16 | fun aiQuery(query: String, closure: AiResponse.() -> Unit) 17 | 18 | fun aiQuery(agent: String, query: String): AiResponse? 19 | 20 | fun aiQuery(agent: String, query: String, closure: AiResponse.() -> Unit) 21 | } 22 | 23 | class AiTraitImpl : AiTrait, 24 | HTTPTrait by HTTPTraitImpl() { 25 | 26 | private val ENDPOINT = "https://api.api.ai/v1/" 27 | private var defaultAgent: String? = null 28 | private val agents = HashMap() 29 | private val session = Random().nextLong() 30 | override var aiSubscriptionKey: String? = null 31 | 32 | override fun registerAiAgent(key: String, lang: String, token: String) { 33 | if (aiSubscriptionKey == null) { 34 | throw RuntimeException("set subscription key first") 35 | } 36 | if (agents.containsKey(key)) { 37 | throw RuntimeException("$key agent is already registered!") 38 | } 39 | agents.put(key, AiAgent(key, lang, token)) 40 | if (defaultAgent == null) { 41 | defaultAgent = key 42 | } 43 | } 44 | 45 | override fun aiQuery(query: String): AiResponse? { 46 | if (defaultAgent == null) { 47 | throw RuntimeException("No Agents registered!") 48 | } 49 | return aiQuery(defaultAgent!!, query) 50 | } 51 | 52 | override fun aiQuery(query: String, closure: AiResponse.() -> Unit) { 53 | aiQuery(query)?.closure() 54 | } 55 | 56 | override fun aiQuery(agent: String, query: String): AiResponse? { 57 | val agentConfig = agents[agent]!! 58 | val req = JSONObject() 59 | req.put("lang", agentConfig.lang) 60 | req.put("query", query) 61 | req.put("sessionId", "$session") 62 | val resp = aiRequest("query", agentConfig, req) 63 | if (resp != null) { 64 | return AiResponse(resp) 65 | } else { 66 | return null 67 | } 68 | } 69 | 70 | override fun aiQuery(agent: String, query: String, closure: AiResponse.() -> Unit) { 71 | aiQuery(agent, query)?.closure() 72 | } 73 | 74 | private fun aiRequest(command: String, agent: AiAgent, request: JSONObject): JSONObject? { 75 | return (urlPostJson(ENDPOINT + command + "?v=20150910", 76 | Json.JsonObject(request), "Authorization", "Bearer ${agent.token}", 77 | "ocp-apim-subscription-key", aiSubscriptionKey!!) as? Json.JsonObject)?.json 78 | } 79 | } 80 | 81 | private data class AiAgent(val key: String, val lang: String, val token: String) 82 | 83 | class AiResponse(val raw: JSONObject) { 84 | 85 | val action: String 86 | val speech: String? 87 | 88 | val pQuery: String? 89 | val pSimplified: String? 90 | val pRequestType: String? 91 | val pSummary: String? 92 | val pTime: Date? 93 | 94 | init { 95 | val result = raw.getJSONObject("result") 96 | action = result.optString("action", "input.unknown") 97 | 98 | if (result.has("fulfillment")) { 99 | val fulfillment = result.getJSONObject("fulfillment") 100 | val speech2 = fulfillment.optString("speech") 101 | if (speech2 == "") { 102 | speech = null 103 | } else { 104 | speech = speech2 105 | } 106 | } else { 107 | speech = null 108 | } 109 | if (result.has("parameters")) { 110 | val p = result.getJSONObject("parameters") 111 | pQuery = p.optString("q") 112 | pSimplified = p.optString("simplified") 113 | pRequestType = p.optString("request_type") 114 | val pSummaryS = p.optString("summary") 115 | if (pSummaryS == "") { 116 | pSummary = null 117 | } else { 118 | pSummary = pSummaryS; 119 | } 120 | } else { 121 | pQuery = null 122 | pSimplified = null 123 | pRequestType = null 124 | pSummary = null 125 | } 126 | 127 | pTime = null; 128 | } 129 | } -------------------------------------------------------------------------------- /actor-bots/src/main/java/im/actor/bots/framework/traits/BugSnagtrait.kt: -------------------------------------------------------------------------------- 1 | package im.actor.bots.framework.traits 2 | 3 | var sharedBugSnagClient: com.bugsnag.Client? = null 4 | 5 | interface BugSnag { 6 | fun logException(e: Throwable?) 7 | } 8 | 9 | class BugSnagImpl() : BugSnag { 10 | 11 | override fun logException(e: Throwable?) { 12 | if (e != null) { 13 | sharedBugSnagClient?.notify(e) 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /actor-bots/src/main/java/im/actor/bots/framework/traits/DispatcherTrait.kt: -------------------------------------------------------------------------------- 1 | package im.actor.bots.framework.traits 2 | 3 | import akka.actor.Actor 4 | import akka.actor.Cancellable 5 | import scala.concurrent.duration.Duration 6 | import java.util.concurrent.TimeUnit 7 | 8 | 9 | interface DispatchTrait { 10 | fun initDispatch(actor: Actor) 11 | fun schedule(message: Any, delay: Long): Cancellable 12 | } 13 | 14 | class DispatchTraitImpl : DispatchTrait { 15 | 16 | private var actor: Actor? = null 17 | 18 | override fun schedule(message: Any, delay: Long): Cancellable { 19 | return schedullerSchedule(message, delay) 20 | } 21 | 22 | override fun initDispatch(actor: Actor) { 23 | this.actor = actor 24 | } 25 | 26 | private fun schedullerSchedule(message: Any, delay: Long): Cancellable { 27 | return actor!!.context().system().scheduler().scheduleOnce(Duration.create(delay, TimeUnit.MILLISECONDS), { 28 | actor!!.self().tell(message, actor!!.self()) 29 | }, actor!!.context().dispatcher()) 30 | } 31 | 32 | } -------------------------------------------------------------------------------- /actor-bots/src/main/java/im/actor/bots/framework/traits/HTTPTrait.kt: -------------------------------------------------------------------------------- 1 | package im.actor.bots.framework.traits 2 | 3 | import org.apache.commons.io.IOUtils 4 | import org.apache.http.client.methods.HttpGet 5 | import org.apache.http.client.methods.HttpPost 6 | import org.apache.http.entity.ContentType 7 | import org.apache.http.entity.StringEntity 8 | import org.apache.http.impl.client.CloseableHttpClient 9 | import org.apache.http.impl.client.HttpClients 10 | import org.json.JSONArray 11 | import org.json.JSONObject 12 | import java.io.IOException 13 | 14 | /** 15 | * HTTP Requesting Trait 16 | */ 17 | interface HTTPTrait { 18 | fun urlGetText(url: String, vararg headers: String): String? 19 | fun urlPostUrlEncodedText(url: String, content: String, vararg headers: String): String? 20 | fun urlPostJson(url: String, content: Json, vararg headers: String): Json? 21 | fun urlGetJson(url: String, vararg headers: String): Json? 22 | } 23 | 24 | class HTTPTraitImpl : HTTPTrait { 25 | 26 | private var client: CloseableHttpClient? = null 27 | 28 | override fun urlGetText(url: String, vararg headers: String): String? { 29 | assumeHttp() 30 | try { 31 | val get = HttpGet(url) 32 | for (i in 0..headers.size / 2 - 1) { 33 | get.addHeader(headers[i * 2], headers[i * 2 + 1]) 34 | } 35 | val res = client!!.execute(get) 36 | val text = IOUtils.toString(res.entity.content) 37 | res.close() 38 | if (res.statusLine.statusCode >= 200 && res.statusLine.statusCode < 300) { 39 | return text 40 | } 41 | return null 42 | } catch (e: IOException) { 43 | e.printStackTrace() 44 | } 45 | 46 | return null 47 | } 48 | 49 | override fun urlPostUrlEncodedText(url: String, content: String, vararg headers: String): String? { 50 | assumeHttp() 51 | try { 52 | val post = HttpPost(url) 53 | for (i in 0..headers.size / 2 - 1) { 54 | post.addHeader(headers[i * 2], headers[i * 2 + 1]) 55 | } 56 | post.entity = StringEntity(content, ContentType.APPLICATION_FORM_URLENCODED) 57 | val res = client!!.execute(post) 58 | val text = IOUtils.toString(res.entity.content) 59 | res.close() 60 | if (res.statusLine.statusCode >= 200 && res.statusLine.statusCode < 300) { 61 | return text 62 | } 63 | return null 64 | } catch (e: IOException) { 65 | e.printStackTrace() 66 | } 67 | 68 | return null 69 | } 70 | 71 | override fun urlPostJson(url: String, content: Json, vararg headers: String): Json? { 72 | assumeHttp() 73 | try { 74 | val post = HttpPost(url) 75 | for (i in 0..headers.size / 2 - 1) { 76 | post.addHeader(headers[i * 2], headers[i * 2 + 1]) 77 | } 78 | post.entity = StringEntity(content.toString(), ContentType.APPLICATION_JSON) 79 | val res = client!!.execute(post) 80 | val text = IOUtils.toString(res.entity.content) 81 | res.close() 82 | if (res.statusLine.statusCode >= 200 && res.statusLine.statusCode < 300) { 83 | return parseJson(text) 84 | } 85 | return null 86 | } catch (e: IOException) { 87 | e.printStackTrace() 88 | } 89 | 90 | return null 91 | } 92 | 93 | override fun urlGetJson(url: String, vararg headers: String): Json? { 94 | val res = urlGetText(url, *headers) ?: return null 95 | try { 96 | return parseJson(res) 97 | } catch (e: Exception) { 98 | e.printStackTrace() 99 | } 100 | 101 | return null 102 | } 103 | 104 | private fun assumeHttp() { 105 | if (client == null) { 106 | client = HttpClients.createDefault() 107 | } 108 | } 109 | } 110 | 111 | fun parseJson(text: String): Json? { 112 | try { 113 | return Json.JsonObject(JSONObject(text)) 114 | } catch(e: Exception) { 115 | 116 | } 117 | try { 118 | return Json.JsonArray(JSONArray(text)) 119 | } catch(e: Exception) { 120 | 121 | } 122 | 123 | return null 124 | } 125 | 126 | sealed class Json { 127 | class JsonObject(val json: JSONObject) : Json() { 128 | override fun toString(): String { 129 | return json.toString() 130 | } 131 | } 132 | 133 | class JsonArray(val json: JSONArray) : Json() { 134 | override fun toString(): String { 135 | return json.toString() 136 | } 137 | } 138 | } -------------------------------------------------------------------------------- /actor-bots/src/main/java/im/actor/bots/framework/traits/I18NTrait.kt: -------------------------------------------------------------------------------- 1 | package im.actor.bots.framework.traits 2 | 3 | import im.actor.bots.framework.i18n.I18NEngine 4 | 5 | interface I18NTrait { 6 | var language: String 7 | fun pickLocale(supported: Array, usersLocales: Array): String 8 | fun initLocalize(fileName: String) 9 | fun localized(key: String): String 10 | } 11 | 12 | fun I18NTrait.initLocalize(name: String, supported: Array, usersLocales: Array) { 13 | val locale = pickLocale(supported, usersLocales) 14 | if (locale == "en") { 15 | language = "en" 16 | initLocalize("$name.properties") 17 | } else { 18 | language = locale 19 | initLocalize("${name}_${locale.capitalize()}.properties") 20 | } 21 | } 22 | 23 | class I18NTraitImpl : I18NTrait { 24 | 25 | override var language: String = "en" 26 | 27 | private var i18n: I18NEngine? = null 28 | 29 | override fun pickLocale(supported: Array, usersLocales: Array): String { 30 | for (ul in usersLocales) { 31 | 32 | // TODO: Implement country checking 33 | val uLang = ul.substring(0, 2) 34 | for (s in supported) { 35 | if (s.substring(0, 2).toLowerCase() == uLang) { 36 | return s.toLowerCase() 37 | } 38 | } 39 | } 40 | return supported[0] 41 | } 42 | 43 | override fun initLocalize(fileName: String) { 44 | i18n = I18NEngine(fileName) 45 | } 46 | 47 | override fun localized(key: String): String { 48 | return i18n!!.pick(key) 49 | } 50 | } -------------------------------------------------------------------------------- /actor-bots/src/main/java/im/actor/bots/framework/traits/LogTrait.kt: -------------------------------------------------------------------------------- 1 | package im.actor.bots.framework.traits 2 | 3 | import akka.actor.Actor 4 | import akka.event.DiagnosticLoggingAdapter 5 | import akka.event.Logging 6 | 7 | /** 8 | * Logging Trait 9 | */ 10 | interface LogTrait { 11 | fun initLog(root: Actor) 12 | fun d(msg: String) 13 | fun v(msg: String) 14 | } 15 | 16 | class LogTraitImpl() : LogTrait { 17 | 18 | private var LOG: DiagnosticLoggingAdapter? = null 19 | 20 | override fun initLog(root: Actor) { 21 | LOG = Logging.apply(root) 22 | } 23 | 24 | 25 | override fun v(msg: String) { 26 | LOG!!.info(msg) 27 | } 28 | 29 | override fun d(msg: String) { 30 | LOG!!.debug(msg) 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /actor-bots/src/main/java/im/actor/bots/framework/traits/ParseTrait.kt: -------------------------------------------------------------------------------- 1 | //package im.actor.bots.framework.traits 2 | // 3 | //import org.json.JSONObject 4 | //import java.net.URLEncoder 5 | // 6 | ///** 7 | // * Parse.com trait. Useful for storing bots's data in casual way. But, before use, try our built-in 8 | // * storage 9 | // */ 10 | //interface ParseTrait { 11 | // fun parseAddObject(className: String, obj: JSONObject): String? 12 | // fun parseUpdateObject(className: String, id: String, obj: JSONObject): Boolean 13 | // fun parseGetObject(className: String, id: String): JSONObject? 14 | // fun parseFindObject(className: String, query: String): JSONObject? 15 | //} 16 | // 17 | //class ParseTraitImpl(val appId: String, val restApiKey: String) : ParseTrait, 18 | // HTTPTrait by HTTPTraitImpl() { 19 | // 20 | // private val ENDPOINT = "https://api.parse.com/1" 21 | // 22 | // override fun parseAddObject(className: String, obj: JSONObject): String? { 23 | // val res = urlPostJson("$ENDPOINT/classes/$className", obj, 24 | // "X-Parse-Application-Id", appId, 25 | // "X-Parse-REST-API-Key", restApiKey) 26 | // if (res != null) { 27 | // return res.getString("objectId") 28 | // } 29 | // return null 30 | // } 31 | // 32 | // override fun parseUpdateObject(className: String, id: String, obj: JSONObject): Boolean { 33 | // val res = urlPostJson("$ENDPOINT/classes/$className/$id", obj, 34 | // "X-Parse-Application-Id", appId, 35 | // "X-Parse-REST-API-Key", restApiKey) 36 | // return res != null 37 | // } 38 | // 39 | // override fun parseGetObject(className: String, id: String): JSONObject? { 40 | // throw NotImplementedError() 41 | // } 42 | // 43 | // override fun parseFindObject(className: String, query: String): JSONObject? { 44 | // val res = urlGetJson("$ENDPOINT/classes/$className?where=${URLEncoder.encode(query)}", 45 | // "X-Parse-Application-Id", appId, 46 | // "X-Parse-REST-API-Key", restApiKey) 47 | // if (res != null) { 48 | // val res = res.optJSONArray("results") 49 | // if (res != null) { 50 | // if (res.length() != 1) { 51 | // return null 52 | // } else { 53 | // return res.getJSONObject(0) 54 | // } 55 | // } 56 | // } 57 | // return null 58 | // } 59 | //} -------------------------------------------------------------------------------- /actor-bots/src/main/java/im/actor/bots/framework/traits/SMTPTrait.kt: -------------------------------------------------------------------------------- 1 | package im.actor.bots.framework.traits 2 | 3 | import com.google.protobuf.StructOrBuilder 4 | import org.codemonkey.simplejavamail.Email 5 | import org.codemonkey.simplejavamail.Mailer 6 | import org.codemonkey.simplejavamail.TransportStrategy 7 | import java.util.* 8 | import javax.mail.* 9 | 10 | interface SMTPTrait { 11 | fun sendEmail(subject: String, text: String, html: String, from: String, fromEmail: String, 12 | to: List, cc: List = ArrayList(), 13 | bcc: List = ArrayList()) 14 | 15 | fun sendEmail(subject: String, text: String, html: String, from: String, fromEmail: String, 16 | to: String) 17 | 18 | fun sendEmail(subject: String, text: String, from: String, fromEmail: String, to: String) 19 | } 20 | 21 | class SMTPTraitIml(val login: String, val password: String, 22 | val smtpHost: String, val smtpPort: Int) : SMTPTrait { 23 | 24 | private var mailer: Mailer? = null 25 | 26 | override fun sendEmail(subject: String, text: String, from: String, fromEmail: String, to: String) { 27 | sendEmail(subject, text, text, from, fromEmail, to) 28 | } 29 | 30 | override fun sendEmail(subject: String, text: String, html: String, from: String, fromEmail: String, to: String) { 31 | var to2 = ArrayList() 32 | to2.add(to) 33 | sendEmail(subject, text, html, from, fromEmail, to2) 34 | } 35 | 36 | override fun sendEmail(subject: String, 37 | text: String, html: String, 38 | from: String, fromEmail: String, 39 | to: List, 40 | cc: List, 41 | bcc: List) { 42 | 43 | val email = Email() 44 | email.setFromAddress(from, fromEmail) 45 | email.subject = subject 46 | email.text = text 47 | email.textHTML = html 48 | for (i in to) { 49 | email.addRecipient(i, i, Message.RecipientType.TO) 50 | } 51 | for (i in cc) { 52 | email.addRecipient(i, i, Message.RecipientType.CC) 53 | } 54 | for (i in bcc) { 55 | email.addRecipient(i, i, Message.RecipientType.BCC) 56 | } 57 | 58 | if (mailer == null) { 59 | mailer = Mailer(smtpHost, smtpPort, login, password, TransportStrategy.SMTP_TLS) 60 | } 61 | mailer!!.sendMail(email) 62 | } 63 | } -------------------------------------------------------------------------------- /actor-bots/src/main/resources/BotFather.properties: -------------------------------------------------------------------------------- 1 | message.start=Hi! I can help you create and manage your Actor bots. \ 2 | Please, read [manual](https://actor.readme.io/docs/bots-getting-started) before we begin.\n \ 3 | Feel free to ask any questions about bots in our OSS Group.\n\n \ 4 | You can control me by sending these commands\n \ 5 | [/newbot](send:/newbot) - creating new bot\n \ 6 | [/setphoto](send:/setphoto) - setting bot's avatar\n \ 7 | [/setname](send:/setname) - setting bot's visible name\n \ 8 | [/setabout](send:/setabout) - setting bot description\n \ 9 | [/list](send:/list) - list your bots\n \ 10 | [/cancel](send:/cancel) - Cancel current operation 11 | 12 | message.new=New bot? Alright. How do we name it? Please, choose a name for your bot bot. 13 | message.new_invalid=Please, send me bot name or *[/cancel](send:/cancel)* for cancel bot creation. 14 | 15 | message.new_username=Good. Now let's choose a username for your bot. 16 | message.new_username_used=Unable to create bot with this username, please, try again with another one. 17 | message.new_username_short=Sorry, but bot username might be at least 5 letters 18 | message.new_username_long=Sorry, but bot username can't be longer than 32 letters 19 | message.new_username_invalid=Please, send me valid bot username. 20 | 21 | message.new_success=Success! Bot's token is $token. Now you can set photo by sending me [/setphoto](send:/setphoto) command 22 | 23 | message.list_empty=You don't have any available bot. You can create new bot with [/newbot](send:/newbot) command. 24 | message.list=Your bots: 25 | 26 | message.edit_name=Please, send me bot's username for name change. 27 | message.edit_name_ask=Ok, now Send me new name for bot. 28 | message.edit_name_ask_invalid=Please, send me valid name! 29 | message.edit_name_error=Unable to change name. Exiting. Please, try again later. 30 | 31 | message.edit_photo=Please, send me bot's username for photo change. 32 | message.edit_photo_ask=Ok, now Send me new photo for bot. 33 | message.edit_photo_ask_invalid=Please, send me valid photo! 34 | message.edit_photo_error=Unable to change photo. Exiting. Please, try again later. 35 | 36 | message.edit_about=Please, send me bot's username for about change. 37 | message.edit_about_ask=Ok, now Send me new about for bot. 38 | message.edit_about_ask_invalid=Please, send me valid about! 39 | message.edit_about_error=Unable to change about. Exiting. Please, try again later. 40 | 41 | message.pick_not_your=This is not your bot! 42 | message.pick_human=Hey! This is not a bot at all! 43 | message.pick_nothing_found=Unable to find bot with username @$text. Please, try again or send [/cancel](send:/cancel) to stop. 44 | 45 | message.success=Success! [Anything else?](send:/start) 46 | message.cancel=Ok. [Anything else?](send:/start) 47 | 48 | message.unknown.0=Command is invalid. Say what? 49 | message.unknown.1=Command is invalid. I really didn't get it... 50 | message.unknown.2=Command is invalid. What do you mean? 51 | message.unknown.3=Command is invalid. Please, say it again in a good way. -------------------------------------------------------------------------------- /actor-bots/src/main/resources/BotFather_Ru.properties: -------------------------------------------------------------------------------- 1 | message.start=Привет! Я здесь что бы помочь тебе создать твоих Ботов в Акторе. \ 2 | Пожалуйста, прочитай [документацию](https://actor.readme.io/docs/bots-getting-started) прежде чем начать.\n \ 3 | И не стесняйся задавать вопросы в группе поддержки OpenSource.\n\n \ 4 | Ты можешь отправлять мне следующие комманды\n \ 5 | [/newbot](send:/newbot) - Создать нового бота\n \ 6 | [/setphoto](send:/setphoto) - Установка изображения бота\n \ 7 | [/setname](send:/setname) - Поменять имя бота\n \ 8 | [/setabout](send:/setabout) - Поменять описание бота\n \ 9 | [/list](send:/list) - Показать список своих ботов 10 | 11 | message.new=Новый бот? Замечательно. Как мы назовем его? Пожалуйста, отправь мне его имя. 12 | message.new_invalid=Пожалуйста, пришли мне имя или [/cancel](send:/cancel) для отмены 13 | 14 | message.new_username=Хорошо. Теперь необходимо выбрать ник для бота. 15 | message.new_username_used=Этот ник уже используется, пожалуйста, попробуй другой. 16 | message.new_username_short=К сожалению, ник должен быть как минимум 5 символов длинной 17 | message.new_username_long=К сожалению, ник не может быть больше 32 символов 18 | message.new_username_invalid=Пожалуйста, пришли мне ник бота. 19 | 20 | message.new_success=Готово! Токен доступа нового бота - $token. Теперь ты можешь установить фото бота, отправив команду [/setphoto](send:/setphoto). 21 | 22 | message.list_empty=У Вас нет зарегистрированных ботов. Создать бота можно командой [/newbot](send:/newbot). 23 | message.list=Ваши боты: 24 | 25 | message.edit_name=Пожалуйста, отправь ник бота. 26 | message.edit_name_ask=Хорошо, теперь новое имя для него. 27 | message.edit_name_ask_invalid=Пожалуйста, пришли мне имя! 28 | message.edit_name_error=Невозможно поменять имя. Пожалуйста, попробуйте позже. 29 | 30 | message.edit_photo=Пожалуйста, отправь ник бота. 31 | message.edit_photo_ask=Хорошо, теперь новое фото для него. 32 | message.edit_photo_ask_invalid=Пожалуйста, пришли мне фото! 33 | message.edit_photo_error=Невозможно поменять фото бота. Пожалуйста, попробуйте позже. 34 | 35 | message.edit_about=Пожалуйста, отправь ник бота. 36 | message.edit_about_ask=Хорошо, теперь новое описание для него. 37 | message.edit_about_ask_invalid=Пожалуйста, пришли мне описание! 38 | message.edit_about_error=Невозможно поменять описание. Пожалуйста, попробуйте позже. 39 | 40 | message.pick_not_your=Это не Ваш бот! 41 | message.pick_human=Хей! Это вообще не бот! 42 | message.pick_nothing_found=Не могу найти бота с ником @$text. Попробуйте еще или отправьте [/cancel](send:/cancel) что бы отменить операцию. 43 | 44 | message.success=Готово! [Что-нибудь еще?](send:/start) 45 | message.cancel=Отменено. [Что-нибудь еще?](send:/start) 46 | 47 | message.unknown.0=Неизвестная команда. Повторите? 48 | message.unknown.1=Неизвестная команда. Я правда не понимаю... 49 | message.unknown.2=Неизвестная команда. Что вы имели ввиду? 50 | message.unknown.3=Неизвестная команда. Пожалуйста, попробуйте вежливее. -------------------------------------------------------------------------------- /actor-bots/src/main/resources/BotFather_Zn.properties: -------------------------------------------------------------------------------- 1 | message.start=您好!我可以帮您创建和管理您的优聆机器人。 我们开始之前,请阅读[手册](https\://actor.readme.io/docs/bots-getting-started)。\n关于优聆机器人的任何问题,欢迎您在我们的开源群中提问。\n\n 您可以通过如下命令控制我:\n [/newbot](send\:/newbot) - 创建新的机器人\n [/setphoto](send\:/setphoto) - 设置机器人的头像\n [/setname](send\:/setname) - 设置机器人的显示名称\n [/setabout](send\:/setabout) - 设置机器人的简介\n [/list](send\:/list) - 列出您的所有机器人\n [/cancel](send\:/cancel) - 取消当前操作 2 | 3 | message.new=新机器人? 好吧,我们怎么命名它呢? 请为您的机器人起个名字。 4 | message.new_invalid=请发机器人的名字给我,或者发送 *[/cancel](send\:/cancel)*来取消机器人的创建。 5 | 6 | message.new_username=好。现在让我们为您的机器人选择昵称。 7 | message.new_username_used=无法使用此昵称创建机器人,请使用其他昵称重试。 8 | message.new_username_short=抱歉,机器人的昵称至少要5个字符。 9 | message.new_username_long=抱歉, 机器人的昵称不能长于32个字符。 10 | message.new_username_invalid=请发给我合法的机器人昵称。 11 | 12 | message.new_success=成功\! 机器人的令牌为: $token。现在您可以向我发送[/setphoto](send\:/setphoto) 命令设置头像。 13 | 14 | message.list_empty=您没有任何可用机器人。您可以使用[/newbot](send\:/newbot)命令创建机器人。 15 | message.list=您的机器人\: 16 | 17 | message.edit_name=为了重命名请发送机器人的昵称给我。 18 | message.edit_name_ask=好的,现在发送机器人的新名字给我。 19 | message.edit_name_ask_invalid=请发送合法的名字给我! 20 | message.edit_name_error=无法更改名称。退出。请稍候再试。 21 | 22 | message.edit_photo=为了更换头像,请发送机器人的昵称给我。 23 | message.edit_photo_ask=好的,现在发送机器人的新头像给我。 24 | message.edit_photo_ask_invalid=请发送合法的头像给我。 25 | message.edit_photo_error=无法更换头像。退出。请稍候再试。 26 | 27 | message.edit_about=为了更新简介,请发送机器人的昵称给我。 28 | message.edit_about_ask=好的,现在发送机器人的新简介给我。 29 | message.edit_about_ask_invalid=请发送合法的简介给我。 30 | message.edit_about_error=无法更新简介。退出。请稍候再试。 31 | 32 | message.pick_not_your=这不是你的机器人! 33 | message.pick_human=您好!这根本不是机器人! 34 | message.pick_nothing_found=无法通过昵称@$text找到机器人。请重试或发送[/cancel](send\:/cancel)取消。 35 | 36 | message.success=成功\! [其他操作?](send\:/start) 37 | message.cancel=好的\! [其他操作?](send\:/start) 38 | 39 | message.unknown.0=命令不合法。您说什么? 40 | message.unknown.1=命令不合法。我真的不明白? 41 | message.unknown.2=命令不合法。您什么意思? 42 | message.unknown.3=命令不合法。请您再好好说一下。 43 | -------------------------------------------------------------------------------- /actor-bots/src/main/resources/reference.conf: -------------------------------------------------------------------------------- 1 | akka.persistence.journal.plugin = "akka.persistence.journal.leveldb" -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/actorapp/actor-bots/ea6c028726b236369b92381462baae752754aedb/build.gradle -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | ## Recomendation 2 | We recommend you to read all guides one by one until you finish [Stateful Bot](tutorials/bot-stateful.md), the most important one. 3 | 4 | ## Guides 5 | * [About Bots](tutorials/bot-about.md) 6 | * [Registering new bot](tutorials/bot-register.md) 7 | * [Implementing bot](tutorials/bot-implement.md) 8 | * [Running bots](tutorials/bot-farm.md) 9 | * [Persistent Bot](tutorials/bot-persistent.md) 10 | * [Stateful Bot](tutorials/bot-stateful.md) 11 | * [Implementing Overlord](tutorials/bot-overlord.md) 12 | * [Incoming WebHooks](tutorials/web-hooks.md) 13 | * [Message Types](tutorials/bot-messages.md) 14 | 15 | ## Modules 16 | * [Bot API Module](api/API.md) 17 | * [Server key-value storage](api/key-value-server.md) 18 | * [Local key-value storage](api/key-value-local.md) 19 | * [Natural language processing with api.ai](api/ai.md) 20 | * [HTTP helpers](api/HTTP.md) 21 | * [i18n support](api/I18N.md) 22 | * [Helpers for admin users of bot](api/admin.md) 23 | 24 | ## Examples 25 | * [BotFather Implementation](../actor-bots/src/main/java/im/actor/bots/embedded/BotFather.kt) 26 | * [Notification bot with Overlord](../actor-bots/src/main/java/im/actor/bots/blocks/Notification.kt) 27 | -------------------------------------------------------------------------------- /docs/api/API.md: -------------------------------------------------------------------------------- 1 | # API Module 2 | 3 | This module provide access to Actor Bot API. 4 | 5 | ```kotlin 6 | interface APITrait { 7 | 8 | // 9 | // Messaging 10 | // 11 | 12 | fun sendText(peer: OutPeer, text: String) 13 | 14 | fun findUser(query: String): BotMessages.User? 15 | 16 | fun getUser(uid: Int): BotMessages.User 17 | 18 | fun getGroup(gid: Int): BotMessages.Group 19 | 20 | fun createGroup(groupTitle: String): BotMessages.ResponseCreateGroup? 21 | 22 | fun inviteUserToGroup(group: BotMessages.GroupOutPeer, user: BotMessages.UserOutPeer): Boolean 23 | 24 | // 25 | // Managing Hooks 26 | // 27 | 28 | fun createHook(hookName: String): String? 29 | 30 | // 31 | // Super Bot methods 32 | // 33 | 34 | fun createBot(userName: String, name: String): BotMessages.BotCreated? 35 | fun changeUserName(uid: Int, name: String): Boolean 36 | fun changeUserAvatar(uid: Int, fileId: Long, accessHash: Long): Boolean 37 | fun changeUserAbout(uid: Int, name: String): Boolean 38 | } 39 | 40 | interface APITraitScoped : APITrait { 41 | fun sendText(text: String) 42 | } 43 | ``` 44 | 45 | ## Messaging API 46 | 47 | #### Sending Text 48 | 49 | ```fun sendText(peer: OutPeer, text: String)``` 50 | 51 | #### Finding user (or bot) by phone number, email or nickname. 52 | 53 | ```fun findUser(query: String): BotMessages.User?``` 54 | 55 | #### Getting cached User object by uid. 56 | NOTE: Cache doesn't saved on disk and lost on actor restart. 57 | 58 | ```fun getUser(uid: Int): BotMessages.User``` 59 | 60 | #### Getting cached Group object by gid. 61 | NOTE: Cache doesn't saved on disk and lost on actor restart. 62 | 63 | ```fun getGroup(gid: Int): BotMessages.Group``` 64 | 65 | ### Creating New Group 66 | ```fun createGroup(groupTitle: String): BotMessages.ResponseCreateGroup?``` 67 | 68 | #### Inviting People to Group 69 | ```fun inviteUserToGroup(group: BotMessages.GroupOutPeer, user: BotMessages.UserOutPeer): Boolean``` 70 | 71 | 72 | 73 | ## WebHooks API 74 | Useful for receiving messages in bot from external services 75 | 76 | #### Create New Hook 77 | Returns null if unable to create hook or it is already exists. 78 | 79 | ```fun createHook(hookName: String): String?``` 80 | 81 | 82 | 83 | ## Super Bot API 84 | This API is only for bots that have admin privilegies. 85 | 86 | #### Create new bot 87 | 88 | ```fun createBot(userName: String, name: String): BotMessages.BotCreated?``` 89 | 90 | #### Change User's name 91 | 92 | ```fun changeUserName(uid: Int, name: String): Boolean``` 93 | 94 | #### Change User's avatar 95 | 96 | ```fun changeUserAvatar(uid: Int, fileId: Long, accessHash: Long): Boolean``` 97 | 98 | #### Change User's about 99 | 100 | ```fun changeUserAbout(uid: Int, name: String): Boolean``` 101 | -------------------------------------------------------------------------------- /docs/api/HTTP.md: -------------------------------------------------------------------------------- 1 | # HTTP Module 2 | 3 | This module contains helpers for making HTTP Requests and JSON-HTTP. 4 | 5 | ```kotlin 6 | interface HTTPTrait { 7 | fun urlGetText(url: String, vararg headers: String): String? 8 | fun urlPostJson(url: String, content: JSONObject, vararg headers: String): JSONObject? 9 | fun urlGetJson(url: String, vararg headers: String): JSONObject? 10 | fun urlGetJsonArray(url: String, vararg headers: String): JSONArray? 11 | } 12 | ``` 13 | 14 | ## Usage 15 | 16 | For GET request, call `fun urlGetText(url: String, vararg headers: String): String?`. Result is not-null if request was successfull and null if not. Headers are array of keys and values. 17 | 18 | For JSON POST request, call `fun urlPostJson(url: String, content: JSONObject, vararg headers: String): JSONObject?` and this method witll post content to `url` with json content type and read response to json object 19 | 20 | For JSON GET requests, call `fun urlGetJson(url: String, vararg headers: String): JSONObject?` or `fun urlGetJsonArray(url: String, vararg headers: String): JSONArray?`. First one if you expect json object and second one if json array. 21 | -------------------------------------------------------------------------------- /docs/api/I18N.md: -------------------------------------------------------------------------------- 1 | # I18N Module 2 | 3 | When writing bot you usually need texts for messages and it is not cool to store them in your code. Also when you want to make your bot global, you usually need to localize your app and this module helps you with this too. 4 | 5 | ```kotlin 6 | interface I18NTrait { 7 | var language: String 8 | fun pickLocale(supported: Array, usersLocales: Array): String 9 | fun initLocalize(fileName: String) 10 | fun initLocalize(name: String, supported: Array, usersLocales: Array) 11 | fun localized(key: String): String 12 | } 13 | ``` 14 | 15 | # Language Files 16 | Language files are simple java .properties files, stored in resoruces directory of your project. 17 | 18 | If you want to show one random text from finite number of text you can write them with random integer in the end and I18N library will rotate them for you 19 | 20 | Example for `message.hello` string: 21 | ``` 22 | message.hello.0=Hello! 23 | message.hello.1=Hi! 24 | message.hello.2=Alloha! 25 | ``` 26 | 27 | # Initialization 28 | 29 | I18N module can run in two modes: localized and plain text. First one is localized version for each user and second one is just useful text source. 30 | 31 | Localization support is enabled by calling `fun initLocalize(name: String, supported: Array, usersLocales: Array)`, where ```name``` is base name of language files, ```supported``` is list of supported languages(lowcase) and ```userLocales``` - destination user locales. 32 | 33 | Language files are simple java .properties files, stored in resoruces path of your project. You can see example in bot father [strings](https://github.com/actorapp/actor-bots/tree/master/actor-bots/src/main/resources). Naming convention for language files is `_.properties` for translations and `.properties` for englis. 34 | 35 | If you don't want to have localization support, just run ```fun initLocalize(fileName: String)``` with exact file name (with .properties extension). 36 | 37 | # Usage 38 | 39 | Jsut call ```fun localized(key: String): String``` to get string. If string is not present exception is thrown. 40 | 41 | In some cases it might be useful to know what locale was selected during init, then you can use `language` variable for this. 42 | -------------------------------------------------------------------------------- /docs/api/admin.md: -------------------------------------------------------------------------------- 1 | # Admin 2 | 3 | This module provide you simple helper for checking if some person is admin of this bot. All admins are hardcoded during bot initialization. 4 | 5 | ## Usage 6 | 7 | In initialization method of your bot register all admins: 8 | 9 | ```kotlin 10 | admins.add("steve") 11 | admins.add("prettynatty") 12 | ``` 13 | 14 | After this you can check if user is admin: 15 | 16 | ```kotlin 17 | isAdmin() 18 | ``` 19 | -------------------------------------------------------------------------------- /docs/api/ai.md: -------------------------------------------------------------------------------- 1 | # Artifical Intelligence 2 | 3 | This module provide you easy access to [api.ai](https://api.ai) to build your Siri-like bot. 4 | 5 | Before begin we recommend to read api.ai [documentation](https://docs.api.ai/) first and understand basic principes. 6 | 7 | ```kotlin 8 | interface AiTrait { 9 | var aiSubscriptionKey: String? 10 | fun registerAiAgent(key: String, lang: String, token: String) 11 | fun aiQuery(query: String): AiResponse? 12 | fun aiQuery(query: String, closure: AiResponse.() -> Unit) 13 | fun aiQuery(agent: String, query: String): AiResponse? 14 | fun aiQuery(agent: String, query: String, closure: AiResponse.() -> Unit) 15 | } 16 | ``` 17 | 18 | ## Query Response 19 | 20 | ```kotlin 21 | class AiResponse { 22 | val raw: JSONObject 23 | val action: String 24 | val speech: String? 25 | 26 | val pQuery: String? 27 | val pSimplified: String? 28 | val pRequestType: String? 29 | val pSummary: String? 30 | val pTime: Date? 31 | } 32 | ``` 33 | 34 | `raw` - Raw Response from API.AI 35 | `action` - Recognized action name 36 | `speech` - Suggested text response 37 | 38 | `pQuery` - query parameter for some actions (like searching on the web) 39 | `pSimplified` - simplified input string. For example, "hi", "hello", "nice to meet you!" will be simplified to "hello" 40 | `pRequestType` - type of request (domain or agent) 41 | `pSummary` - summary of input string 42 | `pTime` - recognized time 43 | 44 | ## Configuration 45 | 46 | Before using ai methods you need to provide subscription key and register your agents. 47 | Module can work with multiple agents. First registered agent is considered as default. 48 | 49 | ```kotlin 50 | aiSubscriptionKey = "" 51 | registerAiAgent("", "", "") 52 | ``` 53 | 54 | ## Queries 55 | 56 | ### Performing AI query 57 | 58 | ```kotlin 59 | fun aiQuery(query: String): AiResponse? 60 | fun aiQuery(agent: String, query: String): AiResponse? 61 | ``` 62 | 63 | ### Query with closure 64 | 65 | ```kotlin 66 | fun aiQuery(query: String, closure: AiResponse.() -> Unit) 67 | fun aiQuery(agent: String, query: String, closure: AiResponse.() -> Unit) 68 | ``` 69 | 70 | Same as above, but provide nice syntax like: 71 | 72 | ```kotlin 73 | aqQuery(text) { 74 | when(action) { 75 | "smalltalk.greetings" -> { 76 | sendText("Hello!") 77 | } 78 | } 79 | } 80 | ``` 81 | -------------------------------------------------------------------------------- /docs/api/key-value-local.md: -------------------------------------------------------------------------------- 1 | # Local Key-Value storage 2 | 3 | For storing information on bot's machine, it is built-in key-value storage based on leveldb. 4 | API is a bit overwhelming and we suggest you to use server key-value instead. 5 | 6 | ## Create KeyValue 7 | 8 | ```kotlin 9 | val stateKeyValue: SimpleKeyValueJava = ShardakkaExtension.get(context().system()).simpleKeyValue("").asJava() 10 | ``` 11 | 12 | ## Using KeyValue 13 | 14 | ```kotlin 15 | 16 | // Reading value 17 | val value = stateKeyValue.get("") 18 | 19 | // Writing value 20 | stateKeyValue.syncUpsert("", ") { 22 | farm("bots") { 23 | bot(EchoBot::class) { 24 | name = "echo" 25 | token = "" 26 | traceHook = "" 27 | overlord = 28 | } 29 | } 30 | } 31 | ``` 32 | -------------------------------------------------------------------------------- /docs/tutorials/bot-implement.md: -------------------------------------------------------------------------------- 1 | # Implementing your first bot 2 | 3 | For fast start, download sources of this repository and import it to IntelliJ IDEA 15 CE. Open actor-bots-example project and add new kotlin file `MyEchoBot.kt`. 4 | 5 | For your first bot, you only need to subclass from MagicBotFork and implement onMessage method: 6 | 7 | ```kotlin 8 | import im.actor.bots.framework.* 9 | 10 | class MyEchoBot(scope: MagicForkScope) : MagicBotFork(scope) { 11 | 12 | override fun onMessage(message: MagicBotMessage) { 13 | when (message) { 14 | is MagicBotTextMessage -> { 15 | sendText("Received: ${message.text}") 16 | } 17 | } 18 | } 19 | } 20 | ``` 21 | 22 | That's all! 23 | 24 | ## Next Step 25 | 26 | Now you can [run](bot-farm.md) your first bot. 27 | -------------------------------------------------------------------------------- /docs/tutorials/bot-messages.md: -------------------------------------------------------------------------------- 1 | # Message Types 2 | 3 | Bot Platform have several message types: text, document (with photo, video, audio extensions), service messages and abstract JSON-message. 4 | 5 | ```kotlin 6 | public abstract class MagicBotMessage(val peer: BotMessages.OutPeer, val sender: BotMessages.UserOutPeer, 7 | val rid: Long) { 8 | 9 | } 10 | 11 | public class MagicBotTextMessage(peer: BotMessages.OutPeer, sender: BotMessages.UserOutPeer, rid: Long, 12 | val text: String) : MagicBotMessage(peer, sender, rid) { 13 | var command: String? = null 14 | var commandArgs: String? = null 15 | } 16 | 17 | public class MagicBotJsonMessage(peer: BotMessages.OutPeer, sender: BotMessages.UserOutPeer, rid: Long, 18 | val json: JSONObject) : MagicBotMessage(peer, sender, rid) { 19 | 20 | } 21 | 22 | public class MagicBotDocMessage(peer: BotMessages.OutPeer, sender: BotMessages.UserOutPeer, rid: Long, 23 | val doc: BotMessages.DocumentMessage) : MagicBotMessage(peer, sender, rid) { 24 | 25 | } 26 | ``` -------------------------------------------------------------------------------- /docs/tutorials/bot-overlord.md: -------------------------------------------------------------------------------- 1 | # Overlord 2 | 3 | Overlord is an Actor that is not binded to specific conversation. Currently overlords is responsible to receiving web hooks. 4 | 5 | ### Implementing 6 | 7 | For implementing overlord, you need to subclass MagicOverlord class and implement required methods: 8 | ```kotlin 9 | class ExampleOverlord(scope: MagicOverlordScope) : MagicOverlord(scope) { 10 | override fun onRawWebHookReceived(name: String, body: ByteArray, headers: JSONObject) { 11 | // Implement Hook Processing 12 | } 13 | } 14 | ``` 15 | 16 | ### Registering 17 | 18 | Also you need to register overlord class: 19 | ```kotlin 20 | farm("BotFarm") { 21 | bot(EchoBot::class) { 22 | name = "echo" 23 | token = "" 24 | traceHook = "" 25 | overlord = ExampleOverlord::class 26 | } 27 | } 28 | ``` 29 | 30 | ### Sending Message to Overlord 31 | 32 | This message can be received in `onReceive` method of overlord. WARRING: Do not forget to call super class method. 33 | 34 | ```kotlin 35 | fun sendToOverlord(object: Any) 36 | ``` 37 | -------------------------------------------------------------------------------- /docs/tutorials/bot-persistent.md: -------------------------------------------------------------------------------- 1 | # Persist bot 2 | 3 | For much easier save/restore of bot's state there is `MagicPersistentBot` that saves it's state to disk after each incoming message. 4 | 5 | Everyting you need is only subclass from `MagicPersistentBot` and implement `onRestoreState` and `onSaveState`. On bot restart onRestoreState is called and you can easily restore saved state. 6 | 7 | ```kotlin 8 | import im.actor.bots.framework.* 9 | import im.actor.bots.framework.persistence.* 10 | import org.json.JSONObject 11 | 12 | class EchoPersistentBot(scope: MagicForkScope) : MagicPersistentBot(scope) { 13 | 14 | var receivedCount: Int = 0 15 | 16 | override fun onRestoreState(state: JSONObject) { 17 | receivedCount = state.optInt("counter", 0) 18 | } 19 | 20 | override fun onMessage(message: MagicBotMessage) { 21 | sendText("Received ${receivedCount++} messages") 22 | } 23 | 24 | override fun onSaveState(state: JSONObject) { 25 | state.put("counter", receivedCount) 26 | } 27 | } 28 | ``` -------------------------------------------------------------------------------- /docs/tutorials/bot-register.md: -------------------------------------------------------------------------------- 1 | # Register bot 2 | 3 | For registering bot, you need to find `@botfather` in Actor Cloud and send him `/start` command. 4 | 5 | After registering new bot, you will get **AUTH_TOKEN** for your bot instance. 6 | 7 | ## Next Step 8 | 9 | Now you can start to implement your [first bot](bot-implement.md). 10 | -------------------------------------------------------------------------------- /docs/tutorials/bot-stateful.md: -------------------------------------------------------------------------------- 1 | # Stateful bot 2 | 3 | Bots are usually have some states. For example, waiting for user command or expecting user input of specific type doin some operation and so on. It is very hard to maintain such states and `MagicStatefulBot` comes to the aid. 4 | 5 | ## Defining States 6 | 7 | When you create subclass from `MagicStatefulBot`, you need to implement `fun configure()` method, where you should define all bots states. States are not dynamically created at the runtime, it's a static tree of states. 8 | 9 | Every state has a name, full form of which is formed by spliting all parents state names with dots. State can easily goto any other state through `goto`, `tryGoto` and `gotoParent` methods. State names passed to these methods can be either short or full. If you pass short state name, method state lookup in all descendants states and it's childs for appropriate state. 10 | 11 | Root state "main" is a state, that expects slash-commands from user. When you need to go to the root state, you can simply call `goto("main")`. 12 | 13 | ## Persistent 14 | 15 | Stateful Bot can persist its state automatically. You can enable this feature by calling `enablePersistent = false` at the beginning of your `configure` method. 16 | 17 | ## Example 18 | 19 | ```kotlin 20 | class ExampleStatefulBot(scope: MagicForkScope) : MagicStatefulBot(scope) { 21 | 22 | override fun configure() { 23 | oneShot("/help") { 24 | sendText("Hello! I am example bot!") 25 | } 26 | raw("/test") { 27 | before { 28 | sendText("Send me something") 29 | } 30 | received { 31 | if (isText) { 32 | sendText("Text received") 33 | } else if (isDoc) { 34 | sendText("Doc received") 35 | } else { 36 | sendText("Something other received") 37 | } 38 | goto("main") 39 | } 40 | } 41 | command("/input") { 42 | before { 43 | sendText("Please, send me text") 44 | goto("ask_text") 45 | } 46 | 47 | expectInput("ask_text") { 48 | 49 | received { 50 | sendText("Yahoo! $text received!") 51 | goto("main") 52 | } 53 | 54 | validate { 55 | if (!isText) { 56 | sendText("I need text!") 57 | return@validate false 58 | } 59 | 60 | return@validate true 61 | } 62 | } 63 | } 64 | } 65 | } 66 | ``` 67 | 68 | ## State types 69 | 70 | ### Expect Commands 71 | 72 | Default initial bot state. ExpectCommands automatically parse commands and route it to particular child state. If there is no state for sent command or it is not a command, state tries to route to state with name "default". 73 | 74 | ```kotlin 75 | expectCommands { 76 | oneShot("/help") { 77 | sendText("Help!") 78 | } 79 | oneShot("/hint") { 80 | sendText("Hint!") 81 | } 82 | } 83 | ``` 84 | 85 | ### One-Shot state 86 | This state initially executes its body and immediately goes to parent. Very useful for responses that doesn't require user input. 87 | 88 | ```kotlin 89 | oneShot("/hint") { 90 | sendText("Hello! I am example bot!") 91 | } 92 | 93 | oneShot("/weather") { 94 | val weather = getUrlJson("weather_service_url") 95 | sendText("Expected temperature ${weather.getString("temperature")}") 96 | } 97 | ``` 98 | 99 | ### Input state 100 | State for expecting user's input. Before entering this state, `before` closure is called. `validate` closure is called within any new message, where you need to validate input text for correctness. If the values will return `true`, `received` closure will be called. 101 | 102 | ```kotlin 103 | expectInput("ask_name") { 104 | before { 105 | sendText("Please, enter your name") 106 | } 107 | received { 108 | sendText("Thank you, $text") 109 | } 110 | validate { 111 | if (!isText) { 112 | sendText("Please, send text") 113 | return@validate false 114 | } 115 | return@validate true 116 | } 117 | } 118 | ``` 119 | 120 | ### Raw state 121 | This is state that doesn't have any specific behaviour. Before entering state `before` closure is executed and on any new message `receive` closure is executed. 122 | 123 | ```kotlin 124 | raw("/translate") { 125 | before { 126 | sendText("Send me messages for translation! and /cancel to stop.") 127 | } 128 | received { 129 | if (isText) { 130 | sendText("Translated ${translate(text)}") 131 | } else if (isCommand) { 132 | if (command == "cancel") { 133 | gotoParent() 134 | } 135 | } 136 | } 137 | 138 | } 139 | ``` 140 | 141 | ### Command state 142 | Obsolete state that acts like `raw`, but marks state as state for some specific command. 143 | -------------------------------------------------------------------------------- /docs/tutorials/web-hooks.md: -------------------------------------------------------------------------------- 1 | # WebHooks Module 2 | 3 | This module allows you to create web hooks for bots and receive information from extenal services. With web hooks you can not only receive data but also implement OAuth2 authentication in external services. 4 | 5 | ### Create New WebHook 6 | 7 | Creating new web hook is simple: you just need to call a method from API Module - ```createHook(name: String): String?``` and in result you will get web hook url. 8 | 9 | ### Receve WebHook 10 | 11 | Receiving web hooks must be implemented in [Overlord](Overlord.md). 12 | 13 | ### Send WebHook 14 | 15 | Sending WebHook is simply POST request to a given URL. Framework pass all headers and binary body to overlord and you can, for example, chekc authentication. 16 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/actorapp/actor-bots/ea6c028726b236369b92381462baae752754aedb/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Oct 02 02:35:09 MSK 2015 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-2.5-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 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 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /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 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 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 Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include 'actor-bots' 2 | include 'actor-bots-example' 3 | 4 | rootProject.name = 'actor-bots-platform' 5 | --------------------------------------------------------------------------------